Почему этот код:

# routing.py

import threading
import time
from typing import List, Dict, Optional
from itertools import cycle
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.
        Simulates processing time.
        """
        if not self.is_alive():
            raise Exception(f"Server {self.server_id} is not alive.")
        # Simulate processing
        time.sleep(request.processing_time)
        with self._lock:
            self._processed_requests.add(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

    def recover(self) -> None:
        """
        Recovers the server.
        """
        with self._lock:
            self._alive = True

    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: List[Server] = list(servers)  # Открытый атрибут для тестов
        self.max_load = max_load
        self.lock = threading.Lock()
        self.load_semaphore = threading.Semaphore(max_load)
        self.client_affinity: Dict[str, Server] = dict()

        # Инициализация Weighted Round Robin
        self.server_weights: List[tuple] = [
            (server, server.performance_score) for server in self.servers if server.is_alive()
        ]
        self.total_weight = sum(weight for _, weight in self.server_weights)
        self.round_robin_cycle = self._weighted_round_robin_cycle()

    def _weighted_round_robin_cycle(self):
        """
        Генератор для Weighted Round Robin.
        """
        while True:
            with self.lock:
                if not self.server_weights:
                    yield None
                for server, weight in self.server_weights:
                    for _ in range(weight):
                        yield server

    def route(self, request: Request) -> None:
        """
        Routes a request to an appropriate server.
        """
        # Ограничение по максимальной нагрузке
        self.load_semaphore.acquire()
        try:
            with self.lock:
                # Проверка клиентской аффинити
                if request.client_id in self.client_affinity:
                    server = self.client_affinity[request.client_id]
                    if not server.is_alive():
                        # Удаление аффинити, если сервер упал
                        del self.client_affinity[request.client_id]
                        server = self._get_next_server()
                        if server:
                            self.client_affinity[request.client_id] = server
                else:
                    server = self._get_next_server()
                    if server:
                        self.client_affinity[request.client_id] = server

            if server:
                server.process_request(request)
            else:
                # Если нет доступных серверов, ожидание
                # В данной реализации запрос просто игнорируется
                pass
        finally:
            self.load_semaphore.release()

    def _get_next_server(self) -> Optional[Server]:
        """
        Получает следующий сервер на основе Weighted Round Robin.
        """
        server = next(self.round_robin_cycle, None)
        while server and not server.is_alive():
            server = next(self.round_robin_cycle, None)
        return server

    def add_server(self, server: Server) -> None:
        """
        Adds a new server to the router.
        """
        with self.lock:
            if server not in self.servers:
                self.servers.append(server)
                if server.is_alive():
                    self.server_weights.append((server, server.performance_score))
                    self.total_weight += server.performance_score
                    # Пересоздание генератора
                    self.round_robin_cycle = self._weighted_round_robin_cycle()

    def remove_server(self, server: Server) -> None:
        """
        Removes a server from the router.
        """
        with self.lock:
            if server in self.servers:
                self.servers.remove(server)
                # Удаление из server_weights
                self.server_weights = [
                    (s, w) for s, w in self.server_weights if s != server
                ]
                self.total_weight = sum(weight for _, weight in self.server_weights)
                # Удаление аффинити для клиентов, привязанных к этому серверу
                clients_to_remove = [
                    client for client, srv in self.client_affinity.items() if srv == server
                ]
                for client in clients_to_remove:
                    del self.client_affinity[client]
                # Пересоздание генератора
                self.round_robin_cycle = self._weighted_round_robin_cycle()

    def crash_server(self, server_id: str) -> None:
        """
        Crashes a server by its ID.
        """
        with self.lock:
            for server in self.servers:
                if server.server_id == server_id:
                    server.crash()
                    break
            # Удаление из server_weights
            self.server_weights = [
                (s, w) for s, w in self.server_weights if s.server_id != server_id
            ]
            self.total_weight = sum(weight for _, weight in self.server_weights)
            # Удаление аффинити для клиентов, привязанных к этому серверу
            clients_to_remove = [
                client for client, srv in self.client_affinity.items() if srv.server_id == server_id
            ]
            for client in clients_to_remove:
                del self.client_affinity[client]
            # Пересоздание генератора
            self.round_robin_cycle = self._weighted_round_robin_cycle()

    def recover_server(self, server_id: str) -> None:
        """
        Recovers a server by its ID.
        """
        with self.lock:
            for server in self.servers:
                if server.server_id == server_id:
                    server.recover()
                    break
            # Добавление обратно в server_weights
            for server in self.servers:
                if server.server_id == server_id and server.is_alive():
                    self.server_weights.append((server, server.performance_score))
            self.total_weight = sum(weight for _, weight in self.server_weights)
            # Пересоздание генератора
            self.round_robin_cycle = self._weighted_round_robin_cycle()

    def get_servers(self) -> List[Server]:
        """
        Returns the list of current servers.
        """
        with self.lock:
            return list(self.servers)

Не решает задачу:

Представьте, что вы разрабатываете систему маршрутизации запросов для интернет-магазина (на самом деле это может быть что угодно). У вас есть 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)