Merge pull request #438 from boxerab/thread-safety-client-manager
Add thread safety to client manager with threading lock
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
@@ -35,6 +36,70 @@ class TestClientManagerAddRemove(unittest.TestCase):
|
|||||||
self.cm.remove_client(ws) # should not raise
|
self.cm.remove_client(ws) # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
class TestClientManagerThreadSafety(unittest.TestCase):
|
||||||
|
def test_concurrent_add_remove(self):
|
||||||
|
cm = ClientManager(max_clients=100, max_connection_time=600)
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
def add_clients(start_idx):
|
||||||
|
try:
|
||||||
|
for i in range(50):
|
||||||
|
ws = MagicMock(name=f"ws-{start_idx}-{i}")
|
||||||
|
client = MagicMock(name=f"client-{start_idx}-{i}")
|
||||||
|
cm.add_client(ws, client)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(e)
|
||||||
|
|
||||||
|
def remove_clients():
|
||||||
|
try:
|
||||||
|
for _ in range(25):
|
||||||
|
with cm.lock:
|
||||||
|
if cm.clients:
|
||||||
|
ws = next(iter(cm.clients))
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
cm.remove_client(ws)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(e)
|
||||||
|
|
||||||
|
threads = [
|
||||||
|
threading.Thread(target=add_clients, args=(0,)),
|
||||||
|
threading.Thread(target=add_clients, args=(1,)),
|
||||||
|
threading.Thread(target=remove_clients),
|
||||||
|
threading.Thread(target=remove_clients),
|
||||||
|
]
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
|
def test_concurrent_get_client(self):
|
||||||
|
cm = ClientManager(max_clients=100, max_connection_time=600)
|
||||||
|
ws = MagicMock()
|
||||||
|
client = MagicMock()
|
||||||
|
cm.add_client(ws, client)
|
||||||
|
errors = []
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def get_many():
|
||||||
|
try:
|
||||||
|
for _ in range(100):
|
||||||
|
results.append(cm.get_client(ws))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(e)
|
||||||
|
|
||||||
|
threads = [threading.Thread(target=get_many) for _ in range(4)]
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertTrue(all(r is client for r in results))
|
||||||
|
|
||||||
|
|
||||||
class TestClientManagerServerFull(unittest.TestCase):
|
class TestClientManagerServerFull(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.cm = ClientManager(max_clients=1, max_connection_time=60)
|
self.cm = ClientManager(max_clients=1, max_connection_time=60)
|
||||||
|
|||||||
+35
-22
@@ -39,6 +39,7 @@ class ClientManager:
|
|||||||
self.start_times = {}
|
self.start_times = {}
|
||||||
self.max_clients = max_clients
|
self.max_clients = max_clients
|
||||||
self.max_connection_time = max_connection_time
|
self.max_connection_time = max_connection_time
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def add_client(self, websocket, client):
|
def add_client(self, websocket, client):
|
||||||
"""
|
"""
|
||||||
@@ -48,8 +49,9 @@ class ClientManager:
|
|||||||
websocket: The websocket associated with the client to add.
|
websocket: The websocket associated with the client to add.
|
||||||
client: The client object to be added and tracked.
|
client: The client object to be added and tracked.
|
||||||
"""
|
"""
|
||||||
self.clients[websocket] = client
|
with self.lock:
|
||||||
self.start_times[websocket] = time.time()
|
self.clients[websocket] = client
|
||||||
|
self.start_times[websocket] = time.time()
|
||||||
|
|
||||||
def get_client(self, websocket):
|
def get_client(self, websocket):
|
||||||
"""
|
"""
|
||||||
@@ -61,9 +63,10 @@ class ClientManager:
|
|||||||
Returns:
|
Returns:
|
||||||
The client object if found, False otherwise.
|
The client object if found, False otherwise.
|
||||||
"""
|
"""
|
||||||
if websocket in self.clients:
|
with self.lock:
|
||||||
return self.clients[websocket]
|
if websocket in self.clients:
|
||||||
return False
|
return self.clients[websocket]
|
||||||
|
return False
|
||||||
|
|
||||||
def remove_client(self, websocket):
|
def remove_client(self, websocket):
|
||||||
"""
|
"""
|
||||||
@@ -73,10 +76,11 @@ class ClientManager:
|
|||||||
Args:
|
Args:
|
||||||
websocket: The websocket associated with the client to be removed.
|
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:
|
if client:
|
||||||
client.cleanup()
|
client.cleanup()
|
||||||
self.start_times.pop(websocket, None)
|
|
||||||
|
|
||||||
def get_wait_time(self):
|
def get_wait_time(self):
|
||||||
"""
|
"""
|
||||||
@@ -85,11 +89,12 @@ class ClientManager:
|
|||||||
Returns:
|
Returns:
|
||||||
The estimated wait time in minutes for new clients to connect. Returns 0 if there are available slots.
|
The estimated wait time in minutes for new clients to connect. Returns 0 if there are available slots.
|
||||||
"""
|
"""
|
||||||
wait_time = None
|
with self.lock:
|
||||||
for start_time in self.start_times.values():
|
wait_time = None
|
||||||
current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
|
for start_time in self.start_times.values():
|
||||||
if wait_time is None or current_client_time_remaining < wait_time:
|
current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
|
||||||
wait_time = current_client_time_remaining
|
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
|
return wait_time / 60 if wait_time is not None else 0
|
||||||
|
|
||||||
def is_server_full(self, websocket, options):
|
def is_server_full(self, websocket, options):
|
||||||
@@ -103,12 +108,18 @@ class ClientManager:
|
|||||||
Returns:
|
Returns:
|
||||||
True if the server is full, False otherwise.
|
True if the server is full, False otherwise.
|
||||||
"""
|
"""
|
||||||
if len(self.clients) >= self.max_clients:
|
with self.lock:
|
||||||
wait_time = self.get_wait_time()
|
if len(self.clients) >= self.max_clients:
|
||||||
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time}
|
wait_time = None
|
||||||
websocket.send(json.dumps(response))
|
for start_time in self.start_times.values():
|
||||||
return True
|
remaining = self.max_connection_time - (time.time() - start_time)
|
||||||
return False
|
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):
|
def is_client_timeout(self, websocket):
|
||||||
"""
|
"""
|
||||||
@@ -120,10 +131,12 @@ class ClientManager:
|
|||||||
Returns:
|
Returns:
|
||||||
True if the client's connection time has exceeded the maximum limit, False otherwise.
|
True if the client's connection time has exceeded the maximum limit, False otherwise.
|
||||||
"""
|
"""
|
||||||
elapsed_time = time.time() - self.start_times[websocket]
|
with self.lock:
|
||||||
if elapsed_time >= self.max_connection_time:
|
elapsed_time = time.time() - self.start_times[websocket]
|
||||||
self.clients[websocket].disconnect()
|
client = self.clients.get(websocket)
|
||||||
logging.warning(f"Client with uid '{self.clients[websocket].client_uid}' disconnected due to overtime.")
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user