Почему этот код:
from typing import List, Dict, Optional
import threading
import time
from collections import defaultdict

class Request:
    def __init__(self, client_id: str, request_id: str, processing_time: float):
        self.client_id = client_id
        self.request_id = request_id
        self.processing_time = processing_time

class Server:
    """
    Represents a server that can process requests.
    """
    def __init__(self, server_id: str, performance_score: int):
        self.server_id = server_id
        self.performance_score = performance_score
        self._alive = True
        self._processed_requests = set()
        self._lock = threading.Lock()

    def process_request(self, request: Request) -> None:
        """
        Processes a request. Save the request client_id and request_id.
        """
        with self._lock:
            if not self._alive:
                raise Exception(f"Server {self.server_id} is down.")
            print(f"Server {self.server_id} processing request {request.request_id} from client {request.client_id}.")
            time.sleep(request.processing_time)
            self._processed_requests.add(request.request_id)
            print(f"Server {self.server_id} completed request {request.request_id}")

    def is_alive(self) -> bool:
        """
        Checks if the server is alive.
        """
        with self._lock:
            return self._alive
    
    def crash(self) -> None:
        """
        Crashes the server.
        """
        with self._lock:
            self._alive = False
            print(f"Server {self.server_id} has crashed.")

    def recover(self) -> None:
        """
        Recovers the server.
        """
        with self._lock:
            self._alive = True
            print(f"Server {self.server_id} has recovered.")


    def is_processed(self, request_id: str) -> bool:
        """
        Checks if the request is processed. Emulates caching.
        """
        with self._lock:
            return request_id in self._processed_requests


class Router:
    """
    Represents a router that can route requests to servers.
    """
    def __init__(self, servers: List[Server], max_load: int):
        self.servers = servers
        self.max_load = max_load
        self.semaphore = threading.Semaphore(max_load)
        self.client_to_server: Dict[str, Server] = {}
        self.lock = threading.Lock()
        self.current_index = 0
        self.server_weights = self._calculate_weights()
        self.total_weight = sum(weight for _, weight in self.server_weights)
        self.server_available = set(server.server_id for server in self.servers if server.is_alive())

    def _calculate_weights(self) -> List[tuple]:
        with self.lock:
            return [(server, server.performance_score) for server in self.servers if server.is_alive()]

    def _get_next_server(self) -> Optional[Server]:
        with self.lock:
            if not self.server_weights:
                return None
        server, weight = self.server_weights[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.server_weights)
        return server


    def route(self, request: Request) -> None:
        self.semaphore.acquire()
        try:
            server = self._select_server(request.client_id)
            if server is None:
                print("No available servers")
                return
            self._handle_request(server, request)
        finally:
            if server is None:
                self.semaphore.release()

    def _select_server(self, client_id: str) -> Optional[Server]:
        with self.lock:
            if client_id in self.client_to_server:
                server = self.client_to_server[client_id]
                if server.is_alive():
                    return server
                else:
                    del self.client_to_server[client_id]

            server = self._get_next_server()
            if server:
                self.client_to_server[client_id] = server
            return server

    def _handle_request(self, server: Server, request: Request) -> None:
        try:
            server.process_request(request)
        except Exception as e:
            print(e)
            with self.lock:
                self._handle_server_failure(server)
        finally:
            self.semaphore.release()


    def _handle_server_failure(self, server: Server) -> None:
        if server.server_id in self.server_available:
            self.server_available.remove(server.server_id)
            self.server_weights = self._calculate_weights()
            self.total_weight = sum(weight for _, weight in self.server_weights)

            affected_clinents = [client for client, srv in self.client_to_server.items() if srv.server_id == server.server_id]
            for client in affected_clinents:
                del self.client_to_server[client]
                print(f"Router updated after server {server.server_id} failure.")
    def add_server(self, server: Server) -> None:
        with self.lock:
            self.servers.append(server)
            if server.is_alive():
                self.server_weights.append((server, server.performance_score))
                self.total_weight += server.performance_score
                self.server_available.add(server.server_id)
            print(f"Server {server.server_id} added to the router")

    def remove_server(self, server: Server) -> None:
        with self.lock:
            if server in self.servers:
                self.servers.remove(server)
            self.server_weights = self._calculate_weights()
            self.total_weight = sum(weight for _, weight in self.server_weights)
            if server.server_id in self.server_available:
                self.server_available.remove(server.server_id)
            affected_clients = [client for client, srv in self.client_to_server.items() if srv.server_id == server.server_id]
            for client in affected_clients:
                del self.client_to_server[client]
            print(f"Server {server.server_id} removed from yhe router.")

    def recover_server(self, server: Server) -> None:
        with self.lock:
            if server in self.servers:
                self.servers.append(server)
            if server.is_alive() and server.server_id not in self.server_available:
                self.server_available.add(server.server_id)
                self.server_weights = self._calculate_weights()
                self.total_weight = sum(weight for _, weight in self.server_weights)
                print(f"Server { server.server_id} has been recovered and added back to the routing")

который должен решать эту задачу:

Представьте, что вы разрабатываете систему маршрутизации запросов для интернет-магазина (на самом деле это может быть что угодно). У вас есть N серверов, которые могут обрабатывать запросы. Каждый сервер имеет разные параметры производительности.

Вам нужно реализовать систему балансировки нагрузки, которая будет распределять запросы между серверами таким образом, чтобы:

Максимально равномерно распределять нагрузку между серверами при параллельных запросах.
Минимизировать количество переключений между серверами (то есть, если сервер обрабатывает запрос, он должен продолжать обрабатывать запрос, пока не закончит или не произойдёт сбой). Если запросы приходят с одного клиента, они всегда должны идти на один и тот же сервер, это будет позволять приложению кешировать данные на стороне сервера.
Обрабатывать сбои серверов, то есть, если сервер недоступен, он должен быть исключен из распределения нагрузки, а когда он станет доступен, он должен быть добавлен обратно.
Максимальная нагрузка на сервер или router не должна превышать max_load, в противном случае сервис должен ждать освобождения ресурсов, не выкидывая ошибок (воспользуйтесь semaphore).
Подсказка: для решения этой задачи можно использовать алгоритм Round Robin, а именно его продвинутый вариант Weighted Round Robin.

Описание задачи

Здесь представлены классы, которые вам нужно будет реализовать:

class Request:
    def __init__(self, client_id: str, request_id: str, processing_time: float):
        self.client_id = client_id
        self.request_id = request_id
        self.processing_time = processing_time
class Server:
    """
    Represents a server that can process requests.
    """
    def __init__(self, server_id: str, performance_score: int):
        self.server_id = server_id
        self.performance_score = performance_score

    def process_request(self, request: Request) -> None:
        """
        Processes a request. Save the request client_id and request_id.
        """
        pass

    def is_alive(self) -> bool:
        """
        Checks if the server is alive.
        """
        pass
    
    def crash(self) -> None:
        """
        Crashes the server.
        """
        pass

    def recover(self) -> None:
        """
        Recovers the server.
        """
        pass


    def is_processed(self, request_id: str) -> bool:
        """
        Checks if the request is processed. Emulates caching.
        """
        pass
class Router:
    """
    Represents a router that can route requests to servers.
    """
    def __init__(self, servers: List[Server], max_load: int):
        pass

    def route(self, request: Request) -> None:
        pass

    def add_server(self, server: Server) -> None:
        pass

    def remove_server(self, server: Server) -> None:
        pass
Немного полезной информации

Round Robin подразумевает хранение списока серверов и выбирает из него по очереди, при достижении конца списка, возвращайтеся в начало (% len(servers))
Чтобы реализовать Weighted Round Robin используйте список кортежей (server, weight), где weight - это вес сервера. Выбирая сервер, выбирайте его с вероятностью пропорциональной весу.
Помните, что роутер должен обрабатывать не более чем max_load запросов.
Для реализации сбоев и восстановлений можно использовать методы crash() и recover() класса Server. При реализации сбоев и восстановлений помните про перерассчет весов для роутинга
Добавление и удаление должно быть реализовано так, чтобы минимальное число клиентов "сменило" сервер при следующем запросе, однако при этом сервера должны быть нагружны равномерно
Для синхронизации доступа к разделяемым ресурсам используйте средства синхронизации из библиотеки threading. Предполагается что все блокировки будут происходить как операция ввода-вывода, поэтому предполагается что маршрутизатор будет использоваться в многопоточном приложении.

Бесконечно зависает на тесте:

def test_router_basic_routing(router):
    request = Request("client1", "request1", 0.1)
    router.route(request)
    
    assert any(server.is_processed("request1") for server in router.servers)