From fafbbf56bf7043821d2cdb0e38e6c009a060351d Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 3 May 2024 09:22:39 +0000 Subject: [PATCH 1/7] feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer --- .gitignore | 3 + changes/2105.feature.md | 1 + configs/manager/halfstack.toml | 8 +- python.lock | 85 ++++++ requirements.txt | 1 + src/ai/backend/common/distributed.py | 74 ++++- src/ai/backend/manager/api/context.py | 24 +- src/ai/backend/manager/api/logs.py | 34 ++- src/ai/backend/manager/cli/__main__.py | 99 ++++++- src/ai/backend/manager/cli/context.py | 28 +- src/ai/backend/manager/config.py | 78 +++++- src/ai/backend/manager/idle.py | 39 ++- src/ai/backend/manager/raft/BUILD | 1 + src/ai/backend/manager/raft/__init__.py | 0 src/ai/backend/manager/raft/logger.py | 25 ++ src/ai/backend/manager/raft/state_machine.py | 50 ++++ src/ai/backend/manager/raft/utils.py | 108 ++++++++ .../backend/manager/scheduler/dispatcher.py | 262 ++++++++++-------- src/ai/backend/manager/server.py | 125 ++++++++- src/ai/backend/manager/types.py | 15 + tests/common/test_distributed.py | 10 +- tests/manager/test_idle_checker.py | 20 +- tests/manager/test_scheduler.py | 3 + 23 files changed, 944 insertions(+), 149 deletions(-) create mode 100644 changes/2105.feature.md create mode 100644 src/ai/backend/manager/raft/BUILD create mode 100644 src/ai/backend/manager/raft/__init__.py create mode 100644 src/ai/backend/manager/raft/logger.py create mode 100644 src/ai/backend/manager/raft/state_machine.py create mode 100644 src/ai/backend/manager/raft/utils.py diff --git a/.gitignore b/.gitignore index 53838cbd808..3235817b85e 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,6 @@ docs/manager/rest-reference/openapi.json /DIST-INFO /INSTALL-INFO + +# Raft cluster config +raft-cluster-config.toml diff --git a/changes/2105.feature.md b/changes/2105.feature.md new file mode 100644 index 00000000000..e5056d80382 --- /dev/null +++ b/changes/2105.feature.md @@ -0,0 +1 @@ +Add Raft-based leader election process to manager group in HA condition in order to make their states consistent. diff --git a/configs/manager/halfstack.toml b/configs/manager/halfstack.toml index 74f079d96c7..7201f391b9f 100644 --- a/configs/manager/halfstack.toml +++ b/configs/manager/halfstack.toml @@ -16,7 +16,7 @@ pool-pre-ping = false [manager] -num-proc = 4 +num-proc = 3 service-addr = { host = "0.0.0.0", port = 8081 } #user = "nobody" #group = "nobody" @@ -35,6 +35,11 @@ hide-agents = true # The order of agent selection. agent-selection-resource-priority = ["cuda", "rocm", "tpu", "cpu", "mem"] +[raft] +heartbeat-tick = 3 +election-tick = 10 +log-dir = "./logs" + [docker-registry] ssl-verify = false @@ -48,6 +53,7 @@ drivers = ["console"] "aiotools" = "INFO" "aiohttp" = "INFO" "ai.backend" = "INFO" +"ai.backend.manager.server.raft" = "INFO" "alembic" = "INFO" "sqlalchemy" = "WARNING" diff --git a/python.lock b/python.lock index 1687c624269..e9b39f2a288 100644 --- a/python.lock +++ b/python.lock @@ -76,6 +76,7 @@ // "python-dotenv~=0.20.0", // "python-json-logger>=2.0.1", // "pyzmq~=25.1.2", +// "raftify==0.1.65", // "redis[hiredis]==4.5.5", // "rich~=13.6", // "setproctitle~=1.3.2", @@ -990,6 +991,7 @@ "artifacts": [ { "algorithm": "sha256", +<<<<<<< HEAD "hash": "b8433d481d50b68a0162c0379c0dd4aabfc3d1ad901800beb5b87815997511c1", "url": "https://files.pythonhosted.org/packages/9a/b0/a4301290ea6cdbb0cda7048ae11b0e560eacca7d2c2e64e6b3d5a9fb3fde/boto3-1.34.144-py3-none-any.whl" }, @@ -997,22 +999,40 @@ "algorithm": "sha256", "hash": "2f3e88b10b8fcc5f6100a9d74cd28230edc9d4fa226d99dd40a3ab38ac213673", "url": "https://files.pythonhosted.org/packages/42/e5/738f7bf96f4f5597c8393e11be2c28bef5f876b5635c1ea9d86888e59657/boto3-1.34.144.tar.gz" +======= + "hash": "6c8125310005255ea998bccc3e8353b4df81a96ab105c89c118461f6c54c07c8", + "url": "https://files.pythonhosted.org/packages/09/18/6b9e0bbdc28a11c1f953160934cd10c938811345d80c3d9c5719c18fe522/boto3-1.34.97-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "60e5dda0b29805fb410bfda1d98e898edaebedac0e6983e9c57cb88e44dfa64e", + "url": "https://files.pythonhosted.org/packages/e5/27/9073116821d6cf73d6463424e3f2d3ab0edaf2ee182c9eb1b263defa3eaf/boto3-1.34.97.tar.gz" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) } ], "project_name": "boto3", "requires_dists": [ +<<<<<<< HEAD "botocore<1.35.0,>=1.34.144", +======= + "botocore<1.35.0,>=1.34.97", +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) "botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "s3transfer<0.11.0,>=0.10.0" ], "requires_python": ">=3.8", +<<<<<<< HEAD "version": "1.34.144" +======= + "version": "1.34.97" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) }, { "artifacts": [ { "algorithm": "sha256", +<<<<<<< HEAD "hash": "a2cf26e1bf10d5917a2285e50257bc44e94a1d16574f282f3274f7a5d8d1f08b", "url": "https://files.pythonhosted.org/packages/24/4b/956a80d406dfffba1f8f7fbaba7dd73d418ed8a7b95faa1ade7cf17663a5/botocore-1.34.144-py3-none-any.whl" }, @@ -1020,6 +1040,15 @@ "algorithm": "sha256", "hash": "4215db28d25309d59c99507f1f77df9089e5bebbad35f6e19c7c44ec5383a3e8", "url": "https://files.pythonhosted.org/packages/8c/66/01d63edf404b2ef2c5594701565ac0c031ce7253231298d423e2514566b8/botocore-1.34.144.tar.gz" +======= + "hash": "c98b1272e377c69e167cc68c0f2c9c79bc7a6098775eecdad41ee5a28de69324", + "url": "https://files.pythonhosted.org/packages/79/2a/cf6ad65939b48a4a3a984174928abab0955e4627a79bf552c876dd17172b/botocore-1.34.97-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e421b592add68547ed141643c8a8b4aa819a07059b85efd72e89b6758c956420", + "url": "https://files.pythonhosted.org/packages/8e/4d/724915591564af6c31bf6a0a41981542634e3b8000a0a1aa61541719c5b1/botocore-1.34.97.tar.gz" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) } ], "project_name": "botocore", @@ -1031,7 +1060,11 @@ "urllib3<1.27,>=1.25.4; python_version < \"3.10\"" ], "requires_python": ">=3.8", +<<<<<<< HEAD "version": "1.34.144" +======= + "version": "1.34.97" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) }, { "artifacts": [ @@ -2539,6 +2572,7 @@ "artifacts": [ { "algorithm": "sha256", +<<<<<<< HEAD "hash": "86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1", "url": "https://files.pythonhosted.org/packages/96/d7/f318261e6ccbba86bdf626e07cd850981508fdaec52cfcdc4ac1030327ab/marshmallow-3.21.3-py3-none-any.whl" }, @@ -2546,6 +2580,15 @@ "algorithm": "sha256", "hash": "4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662", "url": "https://files.pythonhosted.org/packages/d6/31/0881962e77efa2d524ca80566ba1fb7cab000edaa9f4152b97a39b8d9a2d/marshmallow-3.21.3.tar.gz" +======= + "hash": "70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1", + "url": "https://files.pythonhosted.org/packages/be/24/cbb242420021a79c87768dcd22ce028f48ef40913239ad6106c8a557f52c/marshmallow-3.21.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56", + "url": "https://files.pythonhosted.org/packages/a2/16/06ad266adc423f9d7ee49dce26787b973907aa70213760c9fe1711745405/marshmallow-3.21.2.tar.gz" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) } ], "project_name": "marshmallow", @@ -2564,7 +2607,11 @@ "tox; extra == \"dev\"" ], "requires_python": ">=3.8", +<<<<<<< HEAD "version": "3.21.3" +======= + "version": "3.21.2" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) }, { "artifacts": [ @@ -3397,6 +3444,7 @@ "artifacts": [ { "algorithm": "sha256", +<<<<<<< HEAD "hash": "c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", "url": "https://files.pythonhosted.org/packages/4e/e7/81ebdd666d3bff6670d27349b5053605d83d55548e6bd5711f3b0ae7dd23/pytest-8.2.2-py3-none-any.whl" }, @@ -3404,6 +3452,15 @@ "algorithm": "sha256", "hash": "de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977", "url": "https://files.pythonhosted.org/packages/a6/58/e993ca5357553c966b9e73cb3475d9c935fe9488746e13ebdf9b80fae508/pytest-8.2.2.tar.gz" +======= + "hash": "1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", + "url": "https://files.pythonhosted.org/packages/c4/43/6b1debd95ecdf001bc46789a933f658da3f9738c65f32db3f4e8f2a4ca97/pytest-8.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f", + "url": "https://files.pythonhosted.org/packages/09/9d/78b3785134306efe9329f40815af45b9215068d6ae4747ec0bc91ff1f4aa/pytest-8.2.0.tar.gz" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) } ], "project_name": "pytest", @@ -3424,7 +3481,11 @@ "xmlschema; extra == \"dev\"" ], "requires_python": ">=3.8", +<<<<<<< HEAD "version": "8.2.2" +======= + "version": "8.2.0" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) }, { "artifacts": [ @@ -3602,8 +3663,31 @@ "artifacts": [ { "algorithm": "sha256", +<<<<<<< HEAD "hash": "d163680656b34f263fb5074023db44b999c68ff31ab394445ebfd1a2a41fe9a2", "url": "https://files.pythonhosted.org/packages/6b/cd/feba6c20ae4b00d424e6fd802edd4e1557e500501b376c8111a60ba1b83f/readchar-4.1.0-py3-none-any.whl" +======= + "hash": "04c5983aed2c700eb0f3100ac09e51ce5c65ba39d8a627ec138d385261a6a850", + "url": "https://files.pythonhosted.org/packages/78/76/118c5e454a68014e4580cdc2aa0cb87a12c51f40bc57b420ae3daec102ee/raftify-0.1.65-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "71abf9d3e3e829850cba5b013a42c6e9ef2d556585bf224de5aff75a182a93c2", + "url": "https://files.pythonhosted.org/packages/df/e0/49cb08a23f49a232d94fd3a32f534654750930e98cf85d074a9b4be3582b/raftify-0.1.65.tar.gz" + } + ], + "project_name": "raftify", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "0.1.65" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "b4b31dd35de4897be738f27e8f9f62426b5fedb54b648364987e30ae534b71bc", + "url": "https://files.pythonhosted.org/packages/86/db/aca9e5e6a53a499d61cbd78b3594d700f1e48a50ab6970a92a4d1251f8db/readchar-4.0.6-py3-none-any.whl" +>>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) }, { "algorithm": "sha256", @@ -4896,6 +4980,7 @@ "python-dotenv~=0.20.0", "python-json-logger>=2.0.1", "pyzmq~=25.1.2", + "raftify==0.1.65", "redis[hiredis]==4.5.5", "rich~=13.6", "setproctitle~=1.3.2", diff --git a/requirements.txt b/requirements.txt index 3b4bc572a92..e542638d52f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -98,3 +98,4 @@ backend.ai-krunner-alpine==5.2.0 backend.ai-krunner-static-gnu==4.2.0 etcd-client-py==0.3.0 +raftify==0.1.65 diff --git a/src/ai/backend/common/distributed.py b/src/ai/backend/common/distributed.py index 17732610a3b..dd2b9ea988c 100644 --- a/src/ai/backend/common/distributed.py +++ b/src/ai/backend/common/distributed.py @@ -1,10 +1,12 @@ from __future__ import annotations +import abc import asyncio import logging from typing import TYPE_CHECKING, Callable, Final from aiomonitor.task import preserve_termination_log +from raftify import RaftNode from ai.backend.logging import BraceStyleAdapter @@ -16,7 +18,77 @@ log = BraceStyleAdapter(logging.getLogger(__spec__.name)) -class GlobalTimer: +class AbstractGlobalTimer(metaclass=abc.ABCMeta): + @abc.abstractmethod + async def generate_tick(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + async def join(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + async def leave(self) -> None: + raise NotImplementedError + + +class RaftGlobalTimer(AbstractGlobalTimer): + """ + Executes the given async function only once in the given interval, + uniquely among multiple manager instances across multiple nodes. + """ + + _event_producer: Final[EventProducer] + + def __init__( + self, + raft_node: RaftNode, + event_producer: EventProducer, + event_factory: Callable[[], AbstractEvent], + interval: float = 10.0, + initial_delay: float = 0.0, + ) -> None: + self._event_producer = event_producer + self._event_factory = event_factory + self._stopped = False + self.interval = interval + self.initial_delay = initial_delay + self.raft_node = raft_node + + async def generate_tick(self) -> None: + try: + await asyncio.sleep(self.initial_delay) + if self._stopped: + return + while True: + try: + if self._stopped: + return + if await self.raft_node.is_leader(): + await self._event_producer.produce_event(self._event_factory()) + if self._stopped: + return + await asyncio.sleep(self.interval) + except asyncio.TimeoutError: # timeout raised from etcd lock + log.warn("timeout raised while trying to acquire lock. retrying...") + except asyncio.CancelledError: + pass + + async def join(self) -> None: + self._tick_task = asyncio.create_task(self.generate_tick()) + + async def leave(self) -> None: + self._stopped = True + await asyncio.sleep(0) + if not self._tick_task.done(): + try: + self._tick_task.cancel() + await self._tick_task + except asyncio.CancelledError: + pass + + +class DistributedLockGlobalTimer(AbstractGlobalTimer): """ Executes the given async function only once in the given interval, uniquely among multiple manager instances across multiple nodes. diff --git a/src/ai/backend/manager/api/context.py b/src/ai/backend/manager/api/context.py index d8a989b15e1..1ace65ffbc2 100644 --- a/src/ai/backend/manager/api/context.py +++ b/src/ai/backend/manager/api/context.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, cast import attrs +from raftify import Raft, RaftNode if TYPE_CHECKING: from ai.backend.common.bgtask import BackgroundTaskManager @@ -26,6 +27,25 @@ class BaseContext: pass +class RaftClusterContext: + _cluster: Optional[Raft] = None + + def use_raft(self) -> bool: + return self._cluster is not None + + @property + def cluster(self) -> Raft: + return cast(Raft, self._cluster) + + @cluster.setter + def cluster(self, rhs: Raft) -> None: + self._cluster = rhs + + @property + def raft_node(self) -> RaftNode: + return self.cluster.get_raft_node() + + @attrs.define(slots=True, auto_attribs=True, init=False) class RootContext(BaseContext): pidx: int @@ -40,6 +60,7 @@ class RootContext(BaseContext): redis_lock: RedisConnectionInfo shared_config: SharedConfig local_config: LocalConfig + raft_cluster_config: Optional[LocalConfig] cors_options: CORSOptions webapp_plugin_ctx: WebappPluginContext @@ -53,3 +74,4 @@ class RootContext(BaseContext): error_monitor: ErrorPluginContext stats_monitor: StatsPluginContext background_task_manager: BackgroundTaskManager + raft_ctx: RaftClusterContext diff --git a/src/ai/backend/manager/api/logs.py b/src/ai/backend/manager/api/logs.py index cf1a3897158..92d5bb7ba92 100644 --- a/src/ai/backend/manager/api/logs.py +++ b/src/ai/backend/manager/api/logs.py @@ -14,7 +14,11 @@ from dateutil.relativedelta import relativedelta from ai.backend.common import validators as tx -from ai.backend.common.distributed import GlobalTimer +from ai.backend.common.distributed import ( + AbstractGlobalTimer, + DistributedLockGlobalTimer, + RaftGlobalTimer, +) from ai.backend.common.events import AbstractEvent, EmptyEventArgs, EventHandler from ai.backend.common.types import AgentId from ai.backend.logging import BraceStyleAdapter, LogLevel @@ -234,7 +238,7 @@ async def log_cleanup_task(app: web.Application, src: AgentId, event: DoLogClean @attrs.define(slots=True, auto_attribs=True, init=False) class PrivateContext: - log_cleanup_timer: GlobalTimer + log_cleanup_timer: AbstractGlobalTimer log_cleanup_timer_evh: EventHandler[web.Application, DoLogCleanupEvent] @@ -246,14 +250,24 @@ async def init(app: web.Application) -> None: app, log_cleanup_task, ) - app_ctx.log_cleanup_timer = GlobalTimer( - root_ctx.distributed_lock_factory(LockID.LOCKID_LOG_CLEANUP_TIMER, 20.0), - root_ctx.event_producer, - lambda: DoLogCleanupEvent(), - 20.0, - initial_delay=17.0, - task_name="log_cleanup_task", - ) + + if root_ctx.raft_ctx.use_raft(): + app_ctx.log_cleanup_timer = RaftGlobalTimer( + root_ctx.raft_ctx.raft_node, + root_ctx.event_producer, + lambda: DoLogCleanupEvent(), + 20.0, + initial_delay=17.0, + ) + else: + app_ctx.log_cleanup_timer = DistributedLockGlobalTimer( + root_ctx.distributed_lock_factory(LockID.LOCKID_LOG_CLEANUP_TIMER, 20.0), + root_ctx.event_producer, + lambda: DoLogCleanupEvent(), + 20.0, + initial_delay=17.0, + task_name="log_cleanup_task", + ) await app_ctx.log_cleanup_timer.join() diff --git a/src/ai/backend/manager/cli/__main__.py b/src/ai/backend/manager/cli/__main__.py index 8ddbb1051f9..517a024eae2 100644 --- a/src/ai/backend/manager/cli/__main__.py +++ b/src/ai/backend/manager/cli/__main__.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import logging import pathlib import subprocess @@ -8,11 +9,19 @@ import uuid from datetime import datetime from functools import partial -from typing import cast +from typing import Any, cast import click from more_itertools import chunked +from raftify import ( + InitialRole, + Peer, + Peers, + RaftServiceClient, + cli_main, +) from setproctitle import setproctitle +from tabulate import tabulate from ai.backend.cli.params import BoolExprType, OptionalType from ai.backend.cli.types import ExitCode @@ -22,6 +31,7 @@ from ai.backend.logging import BraceStyleAdapter, LogLevel from ai.backend.manager.models import error_logs from ai.backend.manager.models.utils import vacuum_db +from ai.backend.manager.raft.utils import register_custom_deserializer from .context import CLIContext, redis_ctx @@ -349,6 +359,93 @@ async def _clear_old_error_logs(): asyncio.run(vacuum_db(cli_ctx.local_config, vacuum_full)) +async def inspect_node_status(cli_ctx: CLIContext) -> None: + raft_configs = cli_ctx.local_config["raft"] + table = [] + headers = ["ENDPOINT", "NODE ID", "IS LEADER", "RAFT TERM", "RAFT APPLIED INDEX"] + + if raft_configs is not None: + raft_cluster_configs = cli_ctx.raft_cluster_config + assert raft_cluster_configs is not None + + other_peers = [{**peer, "myself": False} for peer in raft_cluster_configs["peers"]["other"]] + my_peers = [{**peer, "myself": True} for peer in raft_cluster_configs["peers"]["myself"]] + all_peers = sorted([*other_peers, *my_peers], key=lambda x: x["node-id"]) + + initial_peers = Peers({ + int(peer_config["node-id"]): Peer( + addr=f"{peer_config['host']}:{peer_config['port']}", + role=InitialRole.from_str(peer_config["role"]), + ) + for peer_config in all_peers + }) + + peers: dict[str, Any] | None = None + for intial_peer in initial_peers.to_dict().values(): + raft_client = await RaftServiceClient.build(intial_peer.get_addr()) + try: + resp = await raft_client.get_peers() + peers = json.loads(resp) + except Exception as e: + print(f"Failed to getting peers from {intial_peer.get_addr()}: {e}") + continue + + if peers is None: + print("No peers are available!") + return + + for node_id in sorted(peers.keys()): + peer = peers[node_id] + raft_client = await RaftServiceClient.build(peer["addr"]) + + try: + node_debugging_info = json.loads(await raft_client.debug_node()) + except Exception as e: + print(f"Failed to getting debugging info from {peer['addr']}: {e}") + table.append([peer["addr"], "(Invalid response)"]) + + is_leader = node_debugging_info["node_id"] == node_debugging_info["leader_id"] + table.append([ + peer["addr"], + node_debugging_info["node_id"], + is_leader, + node_debugging_info["term"], + node_debugging_info["raft_log"]["applied"], + ]) + + table = [headers, *sorted(table, key=lambda x: str(x[0]))] + print( + tabulate(table, headers="firstrow", tablefmt="grid", stralign="center", numalign="center") + ) + + +@main.command() +@click.pass_obj +def status(cli_ctx: CLIContext) -> None: + """ + Collect and print each manager process's status. + """ + asyncio.run(inspect_node_status(cli_ctx)) + + +async def handle_raft_cli_main(argv: list[str]): + await cli_main(argv) + + +@main.command() +@click.pass_obj +@click.argument("args", nargs=-1, type=click.UNPROCESSED) +def raft(cli_ctx: CLIContext, args) -> None: + register_custom_deserializer() + + argv = sys.argv + # Remove "backend.ai", "mgr", "raft" from the argv + argv[:3] = [] + argv.insert(0, "raftify-cli") + + asyncio.run(handle_raft_cli_main(argv)) + + @main.group(cls=LazyGroup, import_name="ai.backend.manager.cli.dbschema:cli") def schema(): """Command set for managing the database schema.""" diff --git a/src/ai/backend/manager/cli/context.py b/src/ai/backend/manager/cli/context.py index 0ab08e6410e..bf2a97d34d3 100644 --- a/src/ai/backend/manager/cli/context.py +++ b/src/ai/backend/manager/cli/context.py @@ -12,24 +12,31 @@ from ai.backend.common import redis_helper from ai.backend.common.config import redis_config_iv -from ai.backend.common.defs import REDIS_IMAGE_DB, REDIS_LIVE_DB, REDIS_STAT_DB, REDIS_STREAM_DB +from ai.backend.common.defs import ( + REDIS_IMAGE_DB, + REDIS_LIVE_DB, + REDIS_STAT_DB, + REDIS_STREAM_DB, +) from ai.backend.common.etcd import AsyncEtcd, ConfigScopes from ai.backend.common.exception import ConfigurationError from ai.backend.common.types import RedisConnectionInfo from ai.backend.logging import AbstractLogger, LocalLogger, LogLevel -from ..config import LocalConfig, SharedConfig +from ..config import LocalConfig, SharedConfig, load_raft_cluster_config from ..config import load as load_config class CLIContext: _local_config: LocalConfig | None + _raft_cluster_config: LocalConfig | None _logger: AbstractLogger def __init__(self, config_path: Path, log_level: LogLevel) -> None: self.config_path = config_path self.log_level = log_level self._local_config = None + self._raft_cluster_config = None @property def local_config(self) -> LocalConfig: @@ -46,6 +53,23 @@ def local_config(self) -> LocalConfig: raise click.Abort() return self._local_config + @property + def raft_cluster_config(self) -> LocalConfig | None: + # Lazy-load the configuration only when requested. + try: + if self._raft_cluster_config is None: + self._raft_cluster_config = load_raft_cluster_config( + self.config_path, self.log_level + ) + except ConfigurationError as e: + print( + "ConfigurationError: Could not read or validate the manager raft cluster config:", + file=sys.stderr, + ) + print(pformat(e.invalid_data), file=sys.stderr) + raise click.Abort() + return self._raft_cluster_config + def __enter__(self) -> Self: # The "start-server" command is injected by ai.backend.cli from the entrypoint # and it has its own multi-process-aware logging initialization. diff --git a/src/ai/backend/manager/config.py b/src/ai/backend/manager/config.py index 8b93b29078d..4b0d2576a4d 100644 --- a/src/ai/backend/manager/config.py +++ b/src/ai/backend/manager/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + """ Configuration Schema on etcd ---------------------------- @@ -172,8 +174,6 @@ - {instance-id}: 1 # just a membership set """ -from __future__ import annotations - import json import logging import os @@ -218,6 +218,7 @@ current_resource_slots, ) from ai.backend.logging import BraceStyleAdapter, LogLevel +from ai.backend.manager.types import RaftNodeInitialRole from ..manager.defs import INTRINSIC_SLOTS from .api import ManagerStatus @@ -315,11 +316,58 @@ t.Key("log-scheduler-ticks", default=False): t.ToBool, t.Key("periodic-sync-stats", default=False): t.ToBool, }).allow_extra("*"), + t.Key("raft", default=None): t.Null + | t.Dict({ + # Storage configurations + t.Key("log-dir"): t.String, + # Raft core configurations + # TODO: Decide proper default values for these configs. + t.Key("heartbeat-tick", default=None): t.Int | t.Null, + t.Key("election-tick", default=None): t.Int | t.Null, + t.Key("min-election-tick", default=None): t.Int | t.Null, + t.Key("max-election-tick", default=None): t.Int | t.Null, + t.Key("max-committed-size-per-ready", default=None): t.Int | t.Null, + t.Key("max-size-per-msg", default=None): t.Int | t.Null, + t.Key("max-inflight-msgs", default=None): t.Int | t.Null, + t.Key("check-quorum", default=None): t.ToBool | t.Null, + t.Key("batch-append", default=None): t.ToBool | t.Null, + t.Key("max-uncommitted-size", default=None): t.Int | t.Null, + t.Key("skip-bcast-commit", default=None): t.ToBool | t.Null, + t.Key("pre-vote", default=None): t.ToBool | t.Null, + t.Key("priority", default=None): t.Int | t.Null, + }).allow_extra("*"), }) .merge(config.etcd_config_iv) .allow_extra("*") ) +manager_raft_cluster_config_iv = t.Dict({ + t.Key("restore-wal-from", default=None): t.Int | t.Null, + t.Key("restore-wal-snapshot-from", default=None): t.Int | t.Null, + t.Key("raft-debug-webserver-enabled", default=False): t.ToBool, + t.Key("peers"): t.Dict({ + t.Key("myself"): t.List( + t.Dict({ + t.Key("node-id"): t.Int, + t.Key("host"): t.String, + t.Key("port"): t.Int, + t.Key("role", default=RaftNodeInitialRole.VOTER): tx.Enum(RaftNodeInitialRole), + }) + ), + t.Key("other", default=[]): ( + t.List( + t.Dict({ + t.Key("node-id"): t.Int, + t.Key("host"): t.String, + t.Key("port"): t.Int, + t.Key("role", default=RaftNodeInitialRole.VOTER): tx.Enum(RaftNodeInitialRole), + }) + ) + | t.Null + ), + }), +}).allow_extra("*") + _config_defaults: Mapping[str, Any] = { "system": { "timezone": "UTC", @@ -360,6 +408,7 @@ "threshold": {}, }, }, + "raft": None, } container_registry_iv = t.Dict({ @@ -588,6 +637,31 @@ def load( return LocalConfig(cfg) +def load_raft_cluster_config( + raft_cluster_config_path: Optional[Path] = None, + log_level: LogLevel = LogLevel.INFO, +) -> Optional[LocalConfig]: + try: + raw_cfg, _ = config.read_from_file(raft_cluster_config_path, "raft-cluster-config") + except config.ConfigurationError: + return None + + try: + cfg = config.check(raw_cfg, manager_raft_cluster_config_iv) + if log_level == LogLevel.DEBUG: + print("== Raft cluster configuration ==", file=sys.stderr) + print(pformat(cfg), file=sys.stderr) + except config.ConfigurationError as e: + print( + "ConfigurationError: Could not read or validate the raft cluster config:", + file=sys.stderr, + ) + print(pformat(e.invalid_data), file=sys.stderr) + raise click.Abort() + else: + return LocalConfig(cfg) + + class SharedConfig(AbstractConfig): ETCD_CONTAINER_REGISTRY_KEY: Final = "config/docker/registry" diff --git a/src/ai/backend/manager/idle.py b/src/ai/backend/manager/idle.py index 55f6e931b48..e30ffdb403a 100644 --- a/src/ai/backend/manager/idle.py +++ b/src/ai/backend/manager/idle.py @@ -39,7 +39,11 @@ import ai.backend.common.validators as tx from ai.backend.common import msgpack, redis_helper from ai.backend.common.defs import REDIS_LIVE_DB, REDIS_STAT_DB -from ai.backend.common.distributed import GlobalTimer +from ai.backend.common.distributed import ( + AbstractGlobalTimer, + DistributedLockGlobalTimer, + RaftGlobalTimer, +) from ai.backend.common.events import ( AbstractEvent, DoIdleCheckEvent, @@ -62,13 +66,14 @@ ) from ai.backend.common.utils import nmget from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.api.context import RaftClusterContext +from ai.backend.manager.types import DistributedLockFactory from .defs import DEFAULT_ROLE, LockID from .models.kernel import LIVE_STATUS, kernels from .models.keypair import keypairs from .models.resource_policy import keypair_resource_policies from .models.user import users -from .types import DistributedLockFactory if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncConnection as SAConnection @@ -179,6 +184,7 @@ class ReportInfo(TypedDict): class IdleCheckerHost: + timer: AbstractGlobalTimer check_interval: ClassVar[float] = DEFAULT_CHECK_INTERVAL def __init__( @@ -187,6 +193,7 @@ def __init__( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, + raft_ctx: RaftClusterContext, lock_factory: DistributedLockFactory, ) -> None: self._checkers: list[BaseIdleChecker] = [] @@ -209,6 +216,7 @@ def __init__( self._grace_period_checker: NewUserGracePeriodChecker = NewUserGracePeriodChecker( event_dispatcher, self._redis_live, self._redis_stat ) + self.raft_ctx = raft_ctx def add_checker(self, checker: BaseIdleChecker): if self._frozen: @@ -228,13 +236,22 @@ async def start(self) -> None: ) for checker in self._checkers: await checker.populate_config(raw_config.get(checker.name) or {}) - self.timer = GlobalTimer( - self._lock_factory(LockID.LOCKID_IDLE_CHECK_TIMER, self.check_interval), - self._event_producer, - lambda: DoIdleCheckEvent(), - self.check_interval, - task_name="idle_checker", - ) + + if self.raft_ctx.use_raft(): + self.timer = RaftGlobalTimer( + self.raft_ctx.raft_node, + self._event_producer, + lambda: DoIdleCheckEvent(), + self.check_interval, + ) + else: + self.timer = DistributedLockGlobalTimer( + self._lock_factory(LockID.LOCKID_IDLE_CHECK_TIMER, self.check_interval), + self._event_producer, + lambda: DoIdleCheckEvent(), + self.check_interval, + ) + self._evh_idle_check = self._event_dispatcher.consume( DoIdleCheckEvent, None, @@ -928,7 +945,7 @@ async def check_idleness( if (window_size <= 0) or (math.isinf(window_size) and window_size > 0): return True - # Wait until the time "interval" is passed after the last udpated time. + # Wait until the time "interval" is passed after the last updated time. t = await redis_helper.execute(self._redis_live, lambda r: r.time()) util_now: float = t[0] + (t[1] / (10**6)) raw_util_last_collected = cast( @@ -1168,6 +1185,7 @@ async def init_idle_checkers( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, + raft_ctx: RaftClusterContext, lock_factory: DistributedLockFactory, ) -> IdleCheckerHost: """ @@ -1179,6 +1197,7 @@ async def init_idle_checkers( shared_config, event_dispatcher, event_producer, + raft_ctx, lock_factory, ) checker_init_args = (event_dispatcher, checker_host._redis_live, checker_host._redis_stat) diff --git a/src/ai/backend/manager/raft/BUILD b/src/ai/backend/manager/raft/BUILD new file mode 100644 index 00000000000..73574424040 --- /dev/null +++ b/src/ai/backend/manager/raft/BUILD @@ -0,0 +1 @@ +python_sources(name="src") diff --git a/src/ai/backend/manager/raft/__init__.py b/src/ai/backend/manager/raft/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/ai/backend/manager/raft/logger.py b/src/ai/backend/manager/raft/logger.py new file mode 100644 index 00000000000..419e4b0dda3 --- /dev/null +++ b/src/ai/backend/manager/raft/logger.py @@ -0,0 +1,25 @@ +from typing import Any + + +class Logger: + def __init__(self, logger: Any) -> None: + self.logger = logger + + def trace(self, message): + self.logger.debug(message) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warn(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def fatal(self, message): + self.logger.error(message) + assert False, "Fatal error occurred: " + message diff --git a/src/ai/backend/manager/raft/state_machine.py b/src/ai/backend/manager/raft/state_machine.py new file mode 100644 index 00000000000..2f48bb8a15c --- /dev/null +++ b/src/ai/backend/manager/raft/state_machine.py @@ -0,0 +1,50 @@ +import asyncio +import pickle +from typing import Optional + + +class SetCommand: + """ + Represent simple key-value command. + Use pickle to serialize the data. + """ + + def __init__(self, key: str, value: str) -> None: + self.key = key + self.value = value + + def encode(self) -> bytes: + return pickle.dumps(self.__dict__) + + @classmethod + def decode(cls, packed: bytes) -> "SetCommand": + unpacked = pickle.loads(packed) + return cls(unpacked["key"], unpacked["value"]) + + +class HashStore: + """ + A simple key-value store that stores data in memory. + Use pickle to serialize the data. + """ + + def __init__(self): + self._store = dict() + self._loop = asyncio.get_running_loop() + + def get(self, key: str) -> Optional[str]: + return self._store.get(key) + + def as_dict(self) -> dict: + return self._store + + async def apply(self, msg: bytes) -> bytes: + message = SetCommand.decode(msg) + self._store[message.key] = message.value + return msg + + async def snapshot(self) -> bytes: + return pickle.dumps(self._store) + + async def restore(self, snapshot: bytes) -> None: + self._store = pickle.loads(snapshot) diff --git a/src/ai/backend/manager/raft/utils.py b/src/ai/backend/manager/raft/utils.py new file mode 100644 index 00000000000..7a0ac5c640b --- /dev/null +++ b/src/ai/backend/manager/raft/utils.py @@ -0,0 +1,108 @@ +import pickle +from typing import Any + +from aiohttp import web +from aiohttp.web import RouteTableDef +from raftify import ( + Raft, + set_confchange_context_deserializer, + set_confchangev2_context_deserializer, + set_entry_context_deserializer, + set_entry_data_deserializer, + set_fsm_deserializer, + set_log_entry_deserializer, + set_message_context_deserializer, + set_snapshot_data_deserializer, +) + +from ai.backend.manager.raft.state_machine import HashStore, SetCommand + +routes = RouteTableDef() +""" +APIs of the web servers to interact with the RaftServers. +""" + + +@routes.get("/get/{id}") +async def get(request: web.Request) -> web.Response: + store: HashStore = request.app["state"]["store"] + id = request.match_info["id"] + return web.Response(text=store.get(id)) + + +@routes.get("/leader") +async def leader(request: web.Request) -> web.Response: + raft: Raft = request.app["state"]["raft"] + leader_id = str(await raft.get_raft_node().get_leader_id()) + return web.Response(text=leader_id) + + +@routes.get("/size") +async def size(request: web.Request) -> web.Response: + raft: Raft = request.app["state"]["raft"] + size = str(await raft.get_raft_node().get_cluster_size()) + return web.Response(text=size) + + +@routes.get("/leave_joint") +async def leave_joint(request: web.Request) -> web.Response: + raft: Raft = request.app["state"]["raft"] + await raft.get_raft_node().leave_joint() + return web.Response(text="OK") + + +@routes.get("/put/{id}/{value}") +async def put(request: web.Request) -> web.Response: + raft: Raft = request.app["state"]["raft"] + id, value = request.match_info["id"], request.match_info["value"] + message = SetCommand(id, value) + + await raft.get_raft_node().propose(message.encode()) + return web.Response(text="OK") + + +class WebServer: + """ + Simple webserver for Raft cluster testing. + Do not use this class for anything other than testing purposes. + """ + + def __init__(self, addr: str, state: dict[str, Any]): + self.app = web.Application() + self.app.add_routes(routes) + self.app["state"] = state + self.host, self.port = addr.split(":") + self.runner = None + + async def run(self): + self.runner = web.AppRunner(self.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, self.host, self.port) + await self.site.start() + + +def pickle_deserialize(data: bytes) -> str | None: + if data == b"": + return None + + if pickle.PROTO in data: + r = pickle.loads(data[data.index(pickle.PROTO) :]) + return r + + # Not pickle data + return None + + +def register_custom_deserializer() -> None: + """ + Initialize the custom deserializers. + """ + + set_confchange_context_deserializer(pickle_deserialize) + set_confchangev2_context_deserializer(pickle_deserialize) + set_entry_context_deserializer(pickle_deserialize) + set_entry_data_deserializer(pickle_deserialize) + set_message_context_deserializer(pickle_deserialize) + set_snapshot_data_deserializer(pickle_deserialize) + set_log_entry_deserializer(pickle_deserialize) + set_fsm_deserializer(pickle_deserialize) diff --git a/src/ai/backend/manager/scheduler/dispatcher.py b/src/ai/backend/manager/scheduler/dispatcher.py index 6eedc637f15..43bf6ed54c8 100644 --- a/src/ai/backend/manager/scheduler/dispatcher.py +++ b/src/ai/backend/manager/scheduler/dispatcher.py @@ -34,7 +34,11 @@ from ai.backend.common import redis_helper from ai.backend.common.defs import REDIS_LIVE_DB -from ai.backend.common.distributed import GlobalTimer +from ai.backend.common.distributed import ( + AbstractGlobalTimer, + DistributedLockGlobalTimer, + RaftGlobalTimer, +) from ai.backend.common.events import ( AgentStartedEvent, CoalescingOptions, @@ -63,6 +67,9 @@ aobject, ) from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.api.context import RaftClusterContext +from ai.backend.manager.defs import SERVICE_MAX_RETRIES, LockID +from ai.backend.manager.models.agent import AgentRow from ai.backend.manager.models.session import _build_session_fetch_query from ai.backend.manager.types import DistributedLockFactory from ai.backend.plugin.entrypoint import scan_entrypoints @@ -74,9 +81,9 @@ SessionNotFound, ) from ..defs import SERVICE_MAX_RETRIES, LockID +from ..api.exceptions import GenericBadRequest, InstanceNotAvailable, SessionNotFound from ..exceptions import convert_to_status_data from ..models import ( - AgentRow, AgentStatus, EndpointLifecycle, EndpointRow, @@ -158,21 +165,22 @@ def load_scheduler( class SchedulerDispatcher(aobject): - config: LocalConfig + local_config: LocalConfig shared_config: SharedConfig registry: AgentRegistry db: SAEngine event_dispatcher: EventDispatcher event_producer: EventProducer - schedule_timer: GlobalTimer - prepare_timer: GlobalTimer - scale_timer: GlobalTimer + schedule_timer: AbstractGlobalTimer + prepare_timer: AbstractGlobalTimer + scale_timer: AbstractGlobalTimer redis_live: RedisConnectionInfo def __init__( self, + raft_ctx: RaftClusterContext, local_config: LocalConfig, shared_config: SharedConfig, event_dispatcher: EventDispatcher, @@ -180,6 +188,7 @@ def __init__( lock_factory: DistributedLockFactory, registry: AgentRegistry, ) -> None: + self.raft_ctx = raft_ctx self.local_config = local_config self.shared_config = shared_config self.event_dispatcher = event_dispatcher @@ -210,32 +219,54 @@ async def __ainit__(self) -> None: evd.consume(DoScheduleEvent, None, self.schedule, coalescing_opts) evd.consume(DoPrepareEvent, None, self.prepare) evd.consume(DoScaleEvent, None, self.scale_services) - self.schedule_timer = GlobalTimer( - self.lock_factory(LockID.LOCKID_SCHEDULE_TIMER, 10.0), - self.event_producer, - lambda: DoScheduleEvent(), - interval=10.0, - task_name="schedule_timer", - ) - self.prepare_timer = GlobalTimer( - self.lock_factory(LockID.LOCKID_PREPARE_TIMER, 10.0), - self.event_producer, - lambda: DoPrepareEvent(), - interval=10.0, - initial_delay=5.0, - task_name="prepare_timer", - ) - self.scale_timer = GlobalTimer( - self.lock_factory(LockID.LOCKID_SCALE_TIMER, 10.0), - self.event_producer, - lambda: DoScaleEvent(), - interval=10.0, - initial_delay=7.0, - task_name="scale_timer", - ) + + if self.raft_ctx.use_raft(): + self.schedule_timer = RaftGlobalTimer( + self.raft_ctx.raft_node, + self.event_producer, + lambda: DoScheduleEvent(), + interval=10.0, + ) + self.prepare_timer = RaftGlobalTimer( + self.raft_ctx.raft_node, + self.event_producer, + lambda: DoPrepareEvent(), + interval=10.0, + initial_delay=5.0, + ) + self.scale_timer = RaftGlobalTimer( + self.raft_ctx.raft_node, + self.event_producer, + lambda: DoScaleEvent(), + interval=10.0, + initial_delay=7.0, + ) + else: + self.schedule_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_SCHEDULE_TIMER, 10.0), + self.event_producer, + lambda: DoScheduleEvent(), + interval=10.0, + ) + self.prepare_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_PREPARE_TIMER, 10.0), + self.event_producer, + lambda: DoPrepareEvent(), + interval=10.0, + initial_delay=5.0, + ) + self.scale_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_SCALE_TIMER, 10.0), + self.event_producer, + lambda: DoScaleEvent(), + interval=10.0, + initial_delay=7.0, + ) + await self.schedule_timer.join() await self.prepare_timer.join() await self.scale_timer.join() + log.info("Session scheduler started") async def close(self) -> None: @@ -243,7 +274,6 @@ async def close(self) -> None: tg.create_task(self.scale_timer.leave()) tg.create_task(self.prepare_timer.leave()) tg.create_task(self.schedule_timer.leave()) - await self.redis_live.close() log.info("Session scheduler stopped") async def schedule( @@ -328,6 +358,23 @@ def _pipeline(r: Redis) -> RedisPipeline: datetime.now(tzutc()).isoformat(), ), ) + result = await db_sess.execute(query) + schedulable_scaling_groups = [row.scaling_group for row in result.fetchall()] + for sgroup_name in schedulable_scaling_groups: + try: + await self._schedule_in_sgroup( + sched_ctx, + sgroup_name, + ) + except InstanceNotAvailable as e: + # Proceed to the next scaling group and come back later. + log.debug( + "schedule({}): instance not available ({})", + sgroup_name, + e.extra_msg, + ) + except Exception as e: + log.exception("schedule({}): scheduling error!\n{}", sgroup_name, repr(e)) except DBAPIError as e: if getattr(e.orig, "pgcode", None) == "55P03": log.info( @@ -1277,91 +1324,90 @@ def _pipeline(r: Redis) -> RedisPipeline: known_slot_types, ) try: - async with self.lock_factory(LockID.LOCKID_PREPARE, 600): - now = datetime.now(tzutc()) + now = datetime.now(tzutc()) - async def _mark_session_preparing() -> Sequence[SessionRow]: - async with self.db.begin_session() as db_sess: - update_query = ( - sa.update(KernelRow) - .values( - status=KernelStatus.PREPARING, - status_changed=now, - status_info="", - status_data={}, - status_history=sql_json_merge( - KernelRow.status_history, - (), - { - KernelStatus.PREPARING.name: now.isoformat(), - }, - ), - ) - .where( - (KernelRow.status == KernelStatus.SCHEDULED), - ) - ) - await db_sess.execute(update_query) - update_sess_query = ( - sa.update(SessionRow) - .values( - status=SessionStatus.PREPARING, - # status_changed=now, - status_info="", - status_data={}, - status_history=sql_json_merge( - SessionRow.status_history, - (), - { - SessionStatus.PREPARING.name: now.isoformat(), - }, - ), - ) - .where(SessionRow.status == SessionStatus.SCHEDULED) - .returning(SessionRow.id) + async def _mark_session_preparing() -> Sequence[SessionRow]: + async with self.db.begin_session() as db_sess: + update_query = ( + sa.update(KernelRow) + .values( + status=KernelStatus.PREPARING, + status_changed=now, + status_info="", + status_data={}, + status_history=sql_json_merge( + KernelRow.status_history, + (), + { + KernelStatus.PREPARING.name: now.isoformat(), + }, + ), ) - rows = (await db_sess.execute(update_sess_query)).fetchall() - if len(rows) == 0: - return [] - target_session_ids = [r["id"] for r in rows] - select_query = ( - sa.select(SessionRow) - .where(SessionRow.id.in_(target_session_ids)) - .options( - noload("*"), - selectinload(SessionRow.kernels).noload("*"), - ) + .where( + (KernelRow.status == KernelStatus.SCHEDULED), ) - result = await db_sess.execute(select_query) - return result.scalars().all() - - scheduled_sessions: Sequence[SessionRow] - scheduled_sessions = await execute_with_retry(_mark_session_preparing) - log.debug("prepare(): preparing {} session(s)", len(scheduled_sessions)) - async with ( - async_timeout.timeout(delay=50.0), - aiotools.PersistentTaskGroup() as tg, - ): - for scheduled_session in scheduled_sessions: - await self.registry.event_producer.produce_event( - SessionPreparingEvent( - scheduled_session.id, - scheduled_session.creation_id, + ) + await db_sess.execute(update_query) + update_sess_query = ( + sa.update(SessionRow) + .values( + status=SessionStatus.PREPARING, + # status_changed=now, + status_info="", + status_data={}, + status_history=sql_json_merge( + SessionRow.status_history, + (), + { + SessionStatus.PREPARING.name: now.isoformat(), + }, ), ) - tg.create_task( - self.start_session( - sched_ctx, - scheduled_session, - ) + .where(SessionRow.status == SessionStatus.SCHEDULED) + .returning(SessionRow.id) + ) + rows = (await db_sess.execute(update_sess_query)).fetchall() + if len(rows) == 0: + return [] + target_session_ids = [r["id"] for r in rows] + select_query = ( + sa.select(SessionRow) + .where(SessionRow.id.in_(target_session_ids)) + .options( + noload("*"), + selectinload(SessionRow.kernels).noload("*"), ) - - await redis_helper.execute( - self.redis_live, - lambda r: r.hset( - redis_key, "resource_group", scheduled_session.scaling_group_name - ), + ) + result = await db_sess.execute(select_query) + return result.scalars().all() + + scheduled_sessions: Sequence[SessionRow] + scheduled_sessions = await execute_with_retry(_mark_session_preparing) + log.debug("prepare(): preparing {} session(s)", len(scheduled_sessions)) + async with ( + async_timeout.timeout(delay=50.0), + aiotools.PersistentTaskGroup() as tg, + ): + for scheduled_session in scheduled_sessions: + await self.registry.event_producer.produce_event( + SessionPreparingEvent( + scheduled_session.id, + scheduled_session.creation_id, + ), + ) + tg.create_task( + self.start_session( + sched_ctx, + scheduled_session, ) + ) + + await redis_helper.execute( + self.redis_live, + lambda r: r.hset( + redis_key, "resource_group", scheduled_session.scaling_group_name + ), + ) await redis_helper.execute( self.redis_live, lambda r: r.hset( diff --git a/src/ai/backend/manager/server.py b/src/ai/backend/manager/server.py index c03b689917a..1d2d776c030 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -22,6 +22,7 @@ List, Mapping, MutableMapping, + Optional, Sequence, cast, ) @@ -31,6 +32,10 @@ import aiotools import click from aiohttp import web +from aiotools import process_index +from raftify import Config as RaftConfig +from raftify import InitialRole, Peer, Peers, Raft +from raftify import RaftConfig as RaftCoreConfig from setproctitle import setproctitle from ai.backend.common import redis_helper @@ -52,11 +57,14 @@ from ai.backend.common.types import AgentSelectionStrategy from ai.backend.common.utils import env_info from ai.backend.logging import BraceStyleAdapter, Logger, LogLevel +from ai.backend.manager.raft.logger import Logger as RaftLogger +from ai.backend.manager.raft.state_machine import HashStore +from ai.backend.manager.raft.utils import WebServer, register_custom_deserializer from . import __version__ from .agent_cache import AgentRPCCache from .api import ManagerStatus -from .api.context import RootContext +from .api.context import RaftClusterContext, RootContext from .api.exceptions import ( BackendError, GenericBadRequest, @@ -71,7 +79,7 @@ WebMiddleware, WebRequestHandler, ) -from .config import LocalConfig, SharedConfig, volume_config_iv +from .config import LocalConfig, SharedConfig, load_raft_cluster_config, volume_config_iv from .config import load as load_config from .exceptions import InvalidArgument from .models import SessionRow @@ -342,8 +350,6 @@ async def manager_status_ctx(root_ctx: RootContext) -> AsyncIterator[None]: @actxmgr async def redis_ctx(root_ctx: RootContext) -> AsyncIterator[None]: - root_ctx.shared_config.data["redis"] - root_ctx.redis_live = redis_helper.get_redis_object( root_ctx.shared_config.data["redis"], name="live", # tracking live status of various entities @@ -434,6 +440,7 @@ async def idle_checker_ctx(root_ctx: RootContext) -> AsyncIterator[None]: root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) await root_ctx.idle_checker_host.start() @@ -512,6 +519,7 @@ async def sched_dispatcher_ctx(root_ctx: RootContext) -> AsyncIterator[None]: from .scheduler.dispatcher import SchedulerDispatcher sched_dispatcher = await SchedulerDispatcher.new( + root_ctx.raft_ctx, root_ctx.local_config, root_ctx.shared_config, root_ctx.event_dispatcher, @@ -658,6 +666,88 @@ async def _force_terminate_hanging_sessions( await task +@actxmgr +async def raft_ctx(root_ctx: RootContext) -> AsyncIterator[None]: + register_custom_deserializer() + raft_configs = root_ctx.local_config.get("raft") + raft_cluster_configs = root_ctx.raft_cluster_config + + if raft_configs is not None: + assert raft_cluster_configs is not None + + other_peers = [{**peer, "myself": False} for peer in raft_cluster_configs["peers"]["other"]] + my_peers = [{**peer, "myself": True} for peer in raft_cluster_configs["peers"]["myself"]] + all_peers = sorted([*other_peers, *my_peers], key=lambda x: x["node-id"]) + + assert ( + root_ctx.local_config["manager"]["num-proc"] >= len(my_peers) + ), "The number of raft peers (myself), should be greater than or equal to the number of processes" + + initial_peers = Peers({ + int(peer_config["node-id"]): Peer( + addr=f"{peer_config['host']}:{peer_config['port']}", + role=InitialRole.from_str(peer_config["role"]), + ) + for peer_config in all_peers + }) + + raft_core_config = RaftCoreConfig( + heartbeat_tick=raft_configs["heartbeat-tick"], + election_tick=raft_configs["election-tick"], + min_election_tick=raft_configs["min-election-tick"], + max_election_tick=raft_configs["max-election-tick"], + max_committed_size_per_ready=raft_configs["max-committed-size-per-ready"], + max_size_per_msg=raft_configs["max-size-per-msg"], + max_inflight_msgs=raft_configs["max-inflight-msgs"], + check_quorum=raft_configs["check-quorum"], + batch_append=raft_configs["batch-append"], + max_uncommitted_size=raft_configs["max-uncommitted-size"], + skip_bcast_commit=raft_configs["skip-bcast-commit"], + pre_vote=raft_configs["pre-vote"], + priority=raft_configs["priority"], + ) + + raft_cfg = RaftConfig( + log_dir=raft_configs["log-dir"], + save_compacted_logs=True, + compacted_log_dir=raft_configs["log-dir"], + restore_wal_from=raft_cluster_configs["restore-wal-from"], + restore_wal_snapshot_from=raft_cluster_configs["restore-wal-snapshot-from"], + initial_peers=initial_peers, + raft_config=raft_core_config, + ) + + node_id_offset = next((idx for idx, item in enumerate(all_peers) if item["myself"]), None) + assert node_id_offset is not None, '"peers.myself" not found in initial_peers!' + node_id = node_id_offset + process_index.get() + 1 + + raft_addr = initial_peers.get(node_id).get_addr() + + store = HashStore() + + raft_logger = RaftLogger( + logging.getLogger(f"{__spec__.name}.raft.node-{node_id}"), # type: ignore + ) + + root_ctx.raft_ctx.cluster = Raft.bootstrap( + node_id, + raft_addr, + store, # type: ignore + raft_cfg, + raft_logger, # type: ignore + ) + raft_cluster = root_ctx.raft_ctx.cluster + raft_cluster.run() # type: ignore + + if raft_cluster_configs["raft-debug-webserver-enabled"]: + # Create webserver only for raft testing + asyncio.create_task( + WebServer(f"127.0.0.1:6025{node_id}", {"raft": raft_cluster, "store": store}).run() + ) + + yield + + class background_task_ctx: def __init__(self, root_ctx: RootContext) -> None: self.root_ctx = root_ctx @@ -772,6 +862,7 @@ def init_lock_factory(root_ctx: RootContext) -> DistributedLockFactory: def build_root_app( pidx: int, local_config: LocalConfig, + raft_cluster_config: Optional[LocalConfig] = None, *, cleanup_contexts: Sequence[CleanupContext] = None, subapp_pkgs: Sequence[str] = None, @@ -790,6 +881,13 @@ def build_root_app( loop.set_exception_handler(global_exception_handler) app["_root.context"] = root_ctx root_ctx.local_config = local_config + root_ctx.raft_cluster_config = raft_cluster_config + + if local_config.get("raft") is not None and raft_cluster_config is None: + raise FileNotFoundError( + "Raft configurations enabled but Raft cluster configuration file not found!" + ) + root_ctx.pidx = pidx root_ctx.cors_options = { "*": aiohttp_cors.ResourceOptions( @@ -815,6 +913,7 @@ def build_root_app( database_ctx, distributed_lock_ctx, event_dispatcher_ctx, + raft_ctx, idle_checker_ctx, storage_manager_ctx, hook_plugin_ctx, @@ -874,8 +973,9 @@ async def server_main( pidx: int, _args: List[Any], ) -> AsyncIterator[None]: - root_app = build_root_app(pidx, _args[0], subapp_pkgs=global_subapp_pkgs) + root_app = build_root_app(pidx, _args[0], _args[1], subapp_pkgs=global_subapp_pkgs) root_ctx: RootContext = root_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() # Start aiomonitor. # Port is set by config (default=50100 + pidx). @@ -954,7 +1054,7 @@ async def server_main_logwrapper( _args: List[Any], ) -> AsyncIterator[None]: setproctitle(f"backend.ai: manager worker-{pidx}") - log_endpoint = _args[1] + log_endpoint = _args[2] logger = Logger( _args[0]["logging"], is_master=False, @@ -964,6 +1064,7 @@ async def server_main_logwrapper( "unpack_opts": DEFAULT_UNPACK_OPTS, }, ) + try: with logger: async with server_main(loop, pidx, _args): @@ -981,6 +1082,13 @@ async def server_main_logwrapper( default=None, help="The config file path. (default: ./manager.toml and /etc/backend.ai/manager.toml)", ) +@click.option( + "--raft-cluster-config-path", + "--raft-cluster-config", + type=Path, + default=None, + help="The raft cluster config file path. (default: ./raft-cluster-config.toml and /etc/backend.ai/raft-cluster-config.toml)", +) @click.option( "--debug", is_flag=True, @@ -997,12 +1105,14 @@ def main( ctx: click.Context, config_path: Path, log_level: LogLevel, + raft_cluster_config_path: Path, debug: bool = False, ) -> None: """ Start the manager service as a foreground process. """ cfg = load_config(config_path, LogLevel.DEBUG if debug else log_level) + raft_cluster_cfg = load_raft_cluster_config(raft_cluster_config_path, log_level) if ctx.invoked_subcommand is None: cfg["manager"]["pid-file"].write_text(str(os.getpid())) @@ -1026,6 +1136,7 @@ def main( log.info("runtime: {0}", env_info()) log_config = logging.getLogger("ai.backend.manager.config") log_config.debug("debug mode enabled.") + if cfg["manager"]["event-loop"] == "uvloop": import uvloop @@ -1035,7 +1146,7 @@ def main( aiotools.start_server( server_main_logwrapper, num_workers=cfg["manager"]["num-proc"], - args=(cfg, log_endpoint), + args=(cfg, raft_cluster_cfg, log_endpoint), wait_timeout=5.0, ) finally: diff --git a/src/ai/backend/manager/types.py b/src/ai/backend/manager/types.py index dee2842dd62..a73d9ef847b 100644 --- a/src/ai/backend/manager/types.py +++ b/src/ai/backend/manager/types.py @@ -56,3 +56,18 @@ class MountOptionModel(BaseModel): MountPermission | None, Field(validation_alias=AliasChoices("permission", "perm"), default=None), ] + + +class RaftNodeInitialRole(str, enum.Enum): + LEADER = "leader" + VOTER = "voter" + LEARNER = "learner" + + +class RaftLogLovel(str, enum.Enum): + TRACE = "trace" + DEBUG = "debug" + INFO = "info" + WARN = "warn" + ERROR = "error" + FATAL = "fatal" diff --git a/tests/common/test_distributed.py b/tests/common/test_distributed.py index 8b769f91699..5d7ada5c27f 100644 --- a/tests/common/test_distributed.py +++ b/tests/common/test_distributed.py @@ -17,7 +17,7 @@ from redis.asyncio import Redis from ai.backend.common import config -from ai.backend.common.distributed import GlobalTimer +from ai.backend.common.distributed import DistributedLockGlobalTimer from ai.backend.common.etcd import AsyncEtcd, ConfigScopes from ai.backend.common.etcd_etcetra import AsyncEtcd as EtcetraAsyncEtcd from ai.backend.common.events import AbstractEvent, EventDispatcher, EventProducer @@ -106,7 +106,7 @@ async def _tick(context: Any, source: AgentId, event: NoopEvent) -> None: ) event_dispatcher.consume(NoopEvent, None, _tick) - timer = GlobalTimer( + timer = DistributedLockGlobalTimer( lock_factory(), event_producer, lambda: NoopEvent(test_case_ns), @@ -176,7 +176,7 @@ async def _tick(context: Any, source: AgentId, event: NoopEvent) -> None: ) etcd_lock = EtcetraLock(etcd_ctx.lock_name, etcetra_etcd, timeout=None, debug=True) - timer = GlobalTimer( + timer = DistributedLockGlobalTimer( etcd_lock, event_producer, lambda: NoopEvent(timer_ctx.test_case_ns), @@ -235,7 +235,7 @@ async def _tick(context: Any, source: AgentId, event: NoopEvent) -> None: ) event_dispatcher.consume(NoopEvent, None, _tick) - timer = GlobalTimer( + timer = DistributedLockGlobalTimer( self.lock_factory(), event_producer, lambda: NoopEvent(self.test_case_ns), @@ -451,7 +451,7 @@ async def _tick(context: Any, source: AgentId, event: NoopEvent) -> None: lock_path = Path(tempfile.gettempdir()) / f"{test_case_ns}.lock" request.addfinalizer(partial(lock_path.unlink, missing_ok=True)) for _ in range(10): - timer = GlobalTimer( + timer = DistributedLockGlobalTimer( FileLock(lock_path, timeout=0, debug=True), event_producer, lambda: NoopEvent(test_case_ns), diff --git a/tests/manager/test_idle_checker.py b/tests/manager/test_idle_checker.py index a84d7d0ca20..b1a03f34432 100644 --- a/tests/manager/test_idle_checker.py +++ b/tests/manager/test_idle_checker.py @@ -7,7 +7,7 @@ from ai.backend.common import msgpack, redis_helper from ai.backend.common.types import KernelId, SessionId, SessionTypes -from ai.backend.manager.api.context import RootContext +from ai.backend.manager.api.context import RaftClusterContext, RootContext from ai.backend.manager.idle import ( BaseIdleChecker, IdleCheckerHost, @@ -97,6 +97,7 @@ async def new_user_grace_period_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() # test config grace_period = 30 @@ -116,6 +117,7 @@ async def new_user_grace_period_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -146,6 +148,7 @@ async def network_timeout_idle_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() # test 1 # remaining time is positive and no grace period @@ -177,6 +180,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -230,6 +234,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -287,6 +292,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -349,6 +355,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -397,6 +404,7 @@ async def session_lifetime_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() # test 1 # remaining time is positive and no grace period @@ -424,6 +432,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -471,6 +480,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -523,6 +533,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -577,6 +588,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) try: @@ -620,6 +632,7 @@ async def utilization_idle_checker__utilization( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() kernel_id = KernelId(uuid4()) expected = { @@ -665,6 +678,7 @@ async def utilization_idle_checker__utilization( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -704,6 +718,7 @@ async def utilization_idle_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] + root_ctx.raft_ctx = RaftClusterContext() # test 1 # remaining time is positive and no utilization. @@ -764,6 +779,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -848,6 +864,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -932,6 +949,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, + root_ctx.raft_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( diff --git a/tests/manager/test_scheduler.py b/tests/manager/test_scheduler.py index cafa6b72f3c..d625c966147 100644 --- a/tests/manager/test_scheduler.py +++ b/tests/manager/test_scheduler.py @@ -26,6 +26,7 @@ SessionId, SessionTypes, ) +from ai.backend.manager.api.context import RaftClusterContext from ai.backend.manager.defs import DEFAULT_ROLE from ai.backend.manager.models.agent import AgentRow from ai.backend.manager.models.image import ImageRow @@ -1105,6 +1106,7 @@ async def test_manually_assign_agent_available( candidate_agents = example_agents example_pending_sessions[0].kernels[0].agent = example_agents[0].id sess_ctx = example_pending_sessions[0] + raft_ctx = RaftClusterContext() dispatcher = SchedulerDispatcher( local_config=mock_local_config, @@ -1112,6 +1114,7 @@ async def test_manually_assign_agent_available( event_dispatcher=mock_event_dispatcher, event_producer=mock_event_producer, lock_factory=file_lock_factory, + raft_ctx=raft_ctx, registry=registry, ) From 1c67576f77c16b58163416acff078875a87f50ba Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Thu, 9 May 2024 09:51:28 +0000 Subject: [PATCH 2/7] fix: Revert unrelated changes from merging with main branch (WIP) --- requirements.txt | 4 +- src/ai/backend/common/distributed.py | 5 + src/ai/backend/common/lock.py | 3 +- src/ai/backend/manager/cli/__main__.py | 2 +- src/ai/backend/manager/config.py | 4 +- src/ai/backend/manager/idle.py | 2 + .../backend/manager/scheduler/dispatcher.py | 180 +++++++++--------- src/ai/backend/manager/server.py | 1 - 8 files changed, 98 insertions(+), 103 deletions(-) diff --git a/requirements.txt b/requirements.txt index e542638d52f..c67a34c4326 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ coloredlogs~=15.0 colorama>=0.4.4 cryptography>=2.8 dataclasses-json~=0.5.7 +etcd-client-py==0.3.0 etcetra~=0.1.19 faker~=24.7.1 graphene~=3.3.0 @@ -59,6 +60,7 @@ PyYAML~=6.0 pydantic~=2.6.4 packaging>=21.3 hiredis>=2.2.3 +raftify==0.1.65 redis[hiredis]==4.5.5 rich~=13.6 SQLAlchemy[postgresql_asyncpg]~=1.4.40 @@ -97,5 +99,3 @@ types-tabulate backend.ai-krunner-alpine==5.2.0 backend.ai-krunner-static-gnu==4.2.0 -etcd-client-py==0.3.0 -raftify==0.1.65 diff --git a/src/ai/backend/common/distributed.py b/src/ai/backend/common/distributed.py index dd2b9ea988c..b387d0bbd1a 100644 --- a/src/ai/backend/common/distributed.py +++ b/src/ai/backend/common/distributed.py @@ -47,12 +47,15 @@ def __init__( event_factory: Callable[[], AbstractEvent], interval: float = 10.0, initial_delay: float = 0.0, + *, + task_name: str | None = None, ) -> None: self._event_producer = event_producer self._event_factory = event_factory self._stopped = False self.interval = interval self.initial_delay = initial_delay + self.task_name = task_name self.raft_node = raft_node async def generate_tick(self) -> None: @@ -76,6 +79,8 @@ async def generate_tick(self) -> None: async def join(self) -> None: self._tick_task = asyncio.create_task(self.generate_tick()) + if self.task_name is not None: + self._tick_task.set_name(self.task_name) async def leave(self) -> None: self._stopped = True diff --git a/src/ai/backend/common/lock.py b/src/ai/backend/common/lock.py index 0824bbba2e6..287a5735685 100644 --- a/src/ai/backend/common/lock.py +++ b/src/ai/backend/common/lock.py @@ -4,10 +4,9 @@ import asyncio import fcntl import logging -from collections.abc import Mapping from io import IOBase from pathlib import Path -from typing import Any, ClassVar, Optional +from typing import Any, ClassVar, Mapping, Optional import trafaret as t from etcd_client import Client as EtcdClient diff --git a/src/ai/backend/manager/cli/__main__.py b/src/ai/backend/manager/cli/__main__.py index 517a024eae2..0c479ebb2ae 100644 --- a/src/ai/backend/manager/cli/__main__.py +++ b/src/ai/backend/manager/cli/__main__.py @@ -387,7 +387,7 @@ async def inspect_node_status(cli_ctx: CLIContext) -> None: resp = await raft_client.get_peers() peers = json.loads(resp) except Exception as e: - print(f"Failed to getting peers from {intial_peer.get_addr()}: {e}") + print(f"Failed to obtain peer information from {intial_peer.get_addr()}: {e}") continue if peers is None: diff --git a/src/ai/backend/manager/config.py b/src/ai/backend/manager/config.py index 4b0d2576a4d..150cf8a86d6 100644 --- a/src/ai/backend/manager/config.py +++ b/src/ai/backend/manager/config.py @@ -1,5 +1,3 @@ -from __future__ import annotations - """ Configuration Schema on etcd ---------------------------- @@ -174,6 +172,8 @@ - {instance-id}: 1 # just a membership set """ +from __future__ import annotations + import json import logging import os diff --git a/src/ai/backend/manager/idle.py b/src/ai/backend/manager/idle.py index e30ffdb403a..b7a119a9fe8 100644 --- a/src/ai/backend/manager/idle.py +++ b/src/ai/backend/manager/idle.py @@ -243,6 +243,7 @@ async def start(self) -> None: self._event_producer, lambda: DoIdleCheckEvent(), self.check_interval, + task_name="idle_checker", ) else: self.timer = DistributedLockGlobalTimer( @@ -250,6 +251,7 @@ async def start(self) -> None: self._event_producer, lambda: DoIdleCheckEvent(), self.check_interval, + task_name="idle_checker", ) self._evh_idle_check = self._event_dispatcher.consume( diff --git a/src/ai/backend/manager/scheduler/dispatcher.py b/src/ai/backend/manager/scheduler/dispatcher.py index 43bf6ed54c8..a6e28619f68 100644 --- a/src/ai/backend/manager/scheduler/dispatcher.py +++ b/src/ai/backend/manager/scheduler/dispatcher.py @@ -226,6 +226,7 @@ async def __ainit__(self) -> None: self.event_producer, lambda: DoScheduleEvent(), interval=10.0, + task_name="schedule_timer", ) self.prepare_timer = RaftGlobalTimer( self.raft_ctx.raft_node, @@ -233,6 +234,7 @@ async def __ainit__(self) -> None: lambda: DoPrepareEvent(), interval=10.0, initial_delay=5.0, + task_name="prepare_timer", ) self.scale_timer = RaftGlobalTimer( self.raft_ctx.raft_node, @@ -240,6 +242,7 @@ async def __ainit__(self) -> None: lambda: DoScaleEvent(), interval=10.0, initial_delay=7.0, + task_name="scale_timer", ) else: self.schedule_timer = DistributedLockGlobalTimer( @@ -247,6 +250,7 @@ async def __ainit__(self) -> None: self.event_producer, lambda: DoScheduleEvent(), interval=10.0, + task_name="schedule_timer", ) self.prepare_timer = DistributedLockGlobalTimer( self.lock_factory(LockID.LOCKID_PREPARE_TIMER, 10.0), @@ -254,6 +258,7 @@ async def __ainit__(self) -> None: lambda: DoPrepareEvent(), interval=10.0, initial_delay=5.0, + task_name="prepare_timer", ) self.scale_timer = DistributedLockGlobalTimer( self.lock_factory(LockID.LOCKID_SCALE_TIMER, 10.0), @@ -261,12 +266,12 @@ async def __ainit__(self) -> None: lambda: DoScaleEvent(), interval=10.0, initial_delay=7.0, + task_name="scale_timer", ) await self.schedule_timer.join() await self.prepare_timer.join() await self.scale_timer.join() - log.info("Session scheduler started") async def close(self) -> None: @@ -274,6 +279,7 @@ async def close(self) -> None: tg.create_task(self.scale_timer.leave()) tg.create_task(self.prepare_timer.leave()) tg.create_task(self.schedule_timer.leave()) + await self.redis_live.close() log.info("Session scheduler stopped") async def schedule( @@ -358,23 +364,6 @@ def _pipeline(r: Redis) -> RedisPipeline: datetime.now(tzutc()).isoformat(), ), ) - result = await db_sess.execute(query) - schedulable_scaling_groups = [row.scaling_group for row in result.fetchall()] - for sgroup_name in schedulable_scaling_groups: - try: - await self._schedule_in_sgroup( - sched_ctx, - sgroup_name, - ) - except InstanceNotAvailable as e: - # Proceed to the next scaling group and come back later. - log.debug( - "schedule({}): instance not available ({})", - sgroup_name, - e.extra_msg, - ) - except Exception as e: - log.exception("schedule({}): scheduling error!\n{}", sgroup_name, repr(e)) except DBAPIError as e: if getattr(e.orig, "pgcode", None) == "55P03": log.info( @@ -1324,90 +1313,91 @@ def _pipeline(r: Redis) -> RedisPipeline: known_slot_types, ) try: - now = datetime.now(tzutc()) + async with self.lock_factory(LockID.LOCKID_PREPARE, 600): + now = datetime.now(tzutc()) - async def _mark_session_preparing() -> Sequence[SessionRow]: - async with self.db.begin_session() as db_sess: - update_query = ( - sa.update(KernelRow) - .values( - status=KernelStatus.PREPARING, - status_changed=now, - status_info="", - status_data={}, - status_history=sql_json_merge( - KernelRow.status_history, - (), - { - KernelStatus.PREPARING.name: now.isoformat(), - }, - ), + async def _mark_session_preparing() -> Sequence[SessionRow]: + async with self.db.begin_session() as db_sess: + update_query = ( + sa.update(KernelRow) + .values( + status=KernelStatus.PREPARING, + status_changed=now, + status_info="", + status_data={}, + status_history=sql_json_merge( + KernelRow.status_history, + (), + { + KernelStatus.PREPARING.name: now.isoformat(), + }, + ), + ) + .where( + (KernelRow.status == KernelStatus.SCHEDULED), + ) ) - .where( - (KernelRow.status == KernelStatus.SCHEDULED), + await db_sess.execute(update_query) + update_sess_query = ( + sa.update(SessionRow) + .values( + status=SessionStatus.PREPARING, + # status_changed=now, + status_info="", + status_data={}, + status_history=sql_json_merge( + SessionRow.status_history, + (), + { + SessionStatus.PREPARING.name: now.isoformat(), + }, + ), + ) + .where(SessionRow.status == SessionStatus.SCHEDULED) + .returning(SessionRow.id) ) - ) - await db_sess.execute(update_query) - update_sess_query = ( - sa.update(SessionRow) - .values( - status=SessionStatus.PREPARING, - # status_changed=now, - status_info="", - status_data={}, - status_history=sql_json_merge( - SessionRow.status_history, - (), - { - SessionStatus.PREPARING.name: now.isoformat(), - }, - ), + rows = (await db_sess.execute(update_sess_query)).fetchall() + if len(rows) == 0: + return [] + target_session_ids = [r["id"] for r in rows] + select_query = ( + sa.select(SessionRow) + .where(SessionRow.id.in_(target_session_ids)) + .options( + noload("*"), + selectinload(SessionRow.kernels).noload("*"), + ) ) - .where(SessionRow.status == SessionStatus.SCHEDULED) - .returning(SessionRow.id) - ) - rows = (await db_sess.execute(update_sess_query)).fetchall() - if len(rows) == 0: - return [] - target_session_ids = [r["id"] for r in rows] - select_query = ( - sa.select(SessionRow) - .where(SessionRow.id.in_(target_session_ids)) - .options( - noload("*"), - selectinload(SessionRow.kernels).noload("*"), + result = await db_sess.execute(select_query) + return result.scalars().all() + + scheduled_sessions: Sequence[SessionRow] + scheduled_sessions = await execute_with_retry(_mark_session_preparing) + log.debug("prepare(): preparing {} session(s)", len(scheduled_sessions)) + async with ( + async_timeout.timeout(delay=50.0), + aiotools.PersistentTaskGroup() as tg, + ): + for scheduled_session in scheduled_sessions: + await self.registry.event_producer.produce_event( + SessionPreparingEvent( + scheduled_session.id, + scheduled_session.creation_id, + ), ) - ) - result = await db_sess.execute(select_query) - return result.scalars().all() - - scheduled_sessions: Sequence[SessionRow] - scheduled_sessions = await execute_with_retry(_mark_session_preparing) - log.debug("prepare(): preparing {} session(s)", len(scheduled_sessions)) - async with ( - async_timeout.timeout(delay=50.0), - aiotools.PersistentTaskGroup() as tg, - ): - for scheduled_session in scheduled_sessions: - await self.registry.event_producer.produce_event( - SessionPreparingEvent( - scheduled_session.id, - scheduled_session.creation_id, - ), - ) - tg.create_task( - self.start_session( - sched_ctx, - scheduled_session, + tg.create_task( + self.start_session( + sched_ctx, + scheduled_session, + ) ) - ) - await redis_helper.execute( - self.redis_live, - lambda r: r.hset( - redis_key, "resource_group", scheduled_session.scaling_group_name - ), - ) + await redis_helper.execute( + self.redis_live, + lambda r: r.hset( + redis_key, "resource_group", scheduled_session.scaling_group_name + ), + ) await redis_helper.execute( self.redis_live, lambda r: r.hset( diff --git a/src/ai/backend/manager/server.py b/src/ai/backend/manager/server.py index 1d2d776c030..2bf416e1467 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -1136,7 +1136,6 @@ def main( log.info("runtime: {0}", env_info()) log_config = logging.getLogger("ai.backend.manager.config") log_config.debug("debug mode enabled.") - if cfg["manager"]["event-loop"] == "uvloop": import uvloop From e7573d06260d35ccf2ddd2112f980270bba53d8e Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 10 May 2024 04:22:52 +0000 Subject: [PATCH 3/7] refactor: Move common logics to `AbstractGlobalTimer` --- src/ai/backend/common/distributed.py | 94 +++++++++++++--------------- src/ai/backend/common/lock.py | 3 +- 2 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/ai/backend/common/distributed.py b/src/ai/backend/common/distributed.py index b387d0bbd1a..342c43013fa 100644 --- a/src/ai/backend/common/distributed.py +++ b/src/ai/backend/common/distributed.py @@ -19,16 +19,46 @@ class AbstractGlobalTimer(metaclass=abc.ABCMeta): - @abc.abstractmethod - async def generate_tick(self) -> None: - raise NotImplementedError + _event_producer: Final[EventProducer] + _event_factory: Final[Callable[[], AbstractEvent]] + _stopped: bool + interval: float + initial_delay: float + task_name: str | None + + def __init__( + self, + event_producer: EventProducer, + event_factory: Callable[[], AbstractEvent], + interval: float = 10.0, + initial_delay: float = 0.0, + *, + task_name: str | None = None, + ) -> None: + self._event_producer = event_producer + self._event_factory = event_factory + self._stopped = False + self.interval = interval + self.initial_delay = initial_delay + self.task_name = task_name - @abc.abstractmethod async def join(self) -> None: - raise NotImplementedError + self._tick_task = asyncio.create_task(self.generate_tick()) + if self.task_name is not None: + self._tick_task.set_name(self.task_name) - @abc.abstractmethod async def leave(self) -> None: + self._stopped = True + await asyncio.sleep(0) + if not self._tick_task.done(): + try: + self._tick_task.cancel() + await self._tick_task + except asyncio.CancelledError: + pass + + @abc.abstractmethod + async def generate_tick(self) -> None: raise NotImplementedError @@ -38,8 +68,6 @@ class RaftGlobalTimer(AbstractGlobalTimer): uniquely among multiple manager instances across multiple nodes. """ - _event_producer: Final[EventProducer] - def __init__( self, raft_node: RaftNode, @@ -50,12 +78,9 @@ def __init__( *, task_name: str | None = None, ) -> None: - self._event_producer = event_producer - self._event_factory = event_factory - self._stopped = False - self.interval = interval - self.initial_delay = initial_delay - self.task_name = task_name + super().__init__( + event_producer, event_factory, interval, initial_delay, task_name=task_name + ) self.raft_node = raft_node async def generate_tick(self) -> None: @@ -77,21 +102,6 @@ async def generate_tick(self) -> None: except asyncio.CancelledError: pass - async def join(self) -> None: - self._tick_task = asyncio.create_task(self.generate_tick()) - if self.task_name is not None: - self._tick_task.set_name(self.task_name) - - async def leave(self) -> None: - self._stopped = True - await asyncio.sleep(0) - if not self._tick_task.done(): - try: - self._tick_task.cancel() - await self._tick_task - except asyncio.CancelledError: - pass - class DistributedLockGlobalTimer(AbstractGlobalTimer): """ @@ -99,8 +109,6 @@ class DistributedLockGlobalTimer(AbstractGlobalTimer): uniquely among multiple manager instances across multiple nodes. """ - _event_producer: Final[EventProducer] - def __init__( self, dist_lock: AbstractDistributedLock, @@ -111,13 +119,10 @@ def __init__( *, task_name: str | None = None, ) -> None: + super().__init__( + event_producer, event_factory, interval, initial_delay, task_name=task_name + ) self._dist_lock = dist_lock - self._event_producer = event_producer - self._event_factory = event_factory - self._stopped = False - self.interval = interval - self.initial_delay = initial_delay - self.task_name = task_name @preserve_termination_log async def generate_tick(self) -> None: @@ -140,18 +145,3 @@ async def generate_tick(self) -> None: log.warning("timeout raised while trying to acquire lock. retrying...") except asyncio.CancelledError: pass - - async def join(self) -> None: - self._tick_task = asyncio.create_task(self.generate_tick()) - if self.task_name is not None: - self._tick_task.set_name(self.task_name) - - async def leave(self) -> None: - self._stopped = True - await asyncio.sleep(0) - if not self._tick_task.done(): - try: - self._tick_task.cancel() - await self._tick_task - except asyncio.CancelledError: - pass diff --git a/src/ai/backend/common/lock.py b/src/ai/backend/common/lock.py index 287a5735685..0824bbba2e6 100644 --- a/src/ai/backend/common/lock.py +++ b/src/ai/backend/common/lock.py @@ -4,9 +4,10 @@ import asyncio import fcntl import logging +from collections.abc import Mapping from io import IOBase from pathlib import Path -from typing import Any, ClassVar, Mapping, Optional +from typing import Any, ClassVar, Optional import trafaret as t from etcd_client import Client as EtcdClient From 7300e9b7aa479da749c5cdbf12beac21510aedce Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 10 May 2024 06:31:07 +0000 Subject: [PATCH 4/7] refactor: Change `raft_cluster_ctx` -> `global_timer_ctx` and expose it more explicitly --- configs/manager/halfstack.toml | 2 + configs/manager/sample.toml | 3 + src/ai/backend/manager/api/context.py | 31 ++--- src/ai/backend/manager/api/logs.py | 40 ++++--- src/ai/backend/manager/cli/__main__.py | 4 +- src/ai/backend/manager/config.py | 4 + src/ai/backend/manager/idle.py | 45 ++++---- src/ai/backend/manager/raft/utils.py | 2 +- .../backend/manager/scheduler/dispatcher.py | 106 +++++++++--------- src/ai/backend/manager/server.py | 26 +++-- tests/manager/test_idle_checker.py | 38 +++---- tests/manager/test_scheduler.py | 6 +- 12 files changed, 167 insertions(+), 140 deletions(-) diff --git a/configs/manager/halfstack.toml b/configs/manager/halfstack.toml index 7201f391b9f..bb5d9419cde 100644 --- a/configs/manager/halfstack.toml +++ b/configs/manager/halfstack.toml @@ -32,6 +32,8 @@ disabled-plugins = [] hide-agents = true +global-timer = "raft" + # The order of agent selection. agent-selection-resource-priority = ["cuda", "rocm", "tpu", "cpu", "mem"] diff --git a/configs/manager/sample.toml b/configs/manager/sample.toml index f42b6c15edb..97f4ab21039 100644 --- a/configs/manager/sample.toml +++ b/configs/manager/sample.toml @@ -129,6 +129,9 @@ agent-selection-resource-priority = ["cuda", "rocm", "tpu", "cpu", "mem"] # compatibility issues. # event-loop = "asyncio" +# One of: "raft", "distributed_lock" +global-timer = "raft" + # One of: "filelock", "pg_advisory", "redlock", "etcd" # Choose the implementation of distributed lock. # "filelock" is the simplest one when the manager is deployed on only one node. diff --git a/src/ai/backend/manager/api/context.py b/src/ai/backend/manager/api/context.py index 1ace65ffbc2..a53ce3bf7b4 100644 --- a/src/ai/backend/manager/api/context.py +++ b/src/ai/backend/manager/api/context.py @@ -1,9 +1,10 @@ from __future__ import annotations +import enum from typing import TYPE_CHECKING, Optional, cast import attrs -from raftify import Raft, RaftNode +from raftify import Raft if TYPE_CHECKING: from ai.backend.common.bgtask import BackgroundTaskManager @@ -27,23 +28,25 @@ class BaseContext: pass -class RaftClusterContext: - _cluster: Optional[Raft] = None +class GlobalTimerKind(enum.StrEnum): + RAFT = "raft" + DISTRIBUTED_LOCK = "distributed-lock" - def use_raft(self) -> bool: - return self._cluster is not None - @property - def cluster(self) -> Raft: - return cast(Raft, self._cluster) +class GlobalTimerContext: + timer_kind: GlobalTimerKind + _raft: Optional[Raft] = None - @cluster.setter - def cluster(self, rhs: Raft) -> None: - self._cluster = rhs + def __init__(self, timer_kind: GlobalTimerKind) -> None: + self.timer_kind = timer_kind @property - def raft_node(self) -> RaftNode: - return self.cluster.get_raft_node() + def raft(self) -> Raft: + return cast(Raft, self._raft) + + @raft.setter + def raft(self, rhs: Raft) -> None: + self._raft = rhs @attrs.define(slots=True, auto_attribs=True, init=False) @@ -74,4 +77,4 @@ class RootContext(BaseContext): error_monitor: ErrorPluginContext stats_monitor: StatsPluginContext background_task_manager: BackgroundTaskManager - raft_ctx: RaftClusterContext + global_timer_ctx: GlobalTimerContext diff --git a/src/ai/backend/manager/api/logs.py b/src/ai/backend/manager/api/logs.py index 92d5bb7ba92..99e1307c8b0 100644 --- a/src/ai/backend/manager/api/logs.py +++ b/src/ai/backend/manager/api/logs.py @@ -20,8 +20,10 @@ RaftGlobalTimer, ) from ai.backend.common.events import AbstractEvent, EmptyEventArgs, EventHandler +from ai.backend.common.logging import BraceStyleAdapter from ai.backend.common.types import AgentId from ai.backend.logging import BraceStyleAdapter, LogLevel +from ai.backend.manager.api.context import GlobalTimerKind from ..defs import LockID from ..models import UserRole, error_logs, groups @@ -251,23 +253,27 @@ async def init(app: web.Application) -> None: log_cleanup_task, ) - if root_ctx.raft_ctx.use_raft(): - app_ctx.log_cleanup_timer = RaftGlobalTimer( - root_ctx.raft_ctx.raft_node, - root_ctx.event_producer, - lambda: DoLogCleanupEvent(), - 20.0, - initial_delay=17.0, - ) - else: - app_ctx.log_cleanup_timer = DistributedLockGlobalTimer( - root_ctx.distributed_lock_factory(LockID.LOCKID_LOG_CLEANUP_TIMER, 20.0), - root_ctx.event_producer, - lambda: DoLogCleanupEvent(), - 20.0, - initial_delay=17.0, - task_name="log_cleanup_task", - ) + match root_ctx.global_timer_ctx.timer_kind: + case GlobalTimerKind.RAFT: + app_ctx.log_cleanup_timer = RaftGlobalTimer( + root_ctx.global_timer_ctx.raft.get_raft_node(), + root_ctx.event_producer, + lambda: DoLogCleanupEvent(), + 20.0, + initial_delay=17.0, + task_name="log_cleanup_task", + ) + case GlobalTimerKind.DISTRIBUTED_LOCK: + app_ctx.log_cleanup_timer = DistributedLockGlobalTimer( + root_ctx.distributed_lock_factory(LockID.LOCKID_LOG_CLEANUP_TIMER, 20.0), + root_ctx.event_producer, + lambda: DoLogCleanupEvent(), + 20.0, + initial_delay=17.0, + task_name="log_cleanup_task", + ) + case _: + assert False, f"Unknown global timer backend: {root_ctx.global_timer_ctx.timer_kind}" await app_ctx.log_cleanup_timer.join() diff --git a/src/ai/backend/manager/cli/__main__.py b/src/ai/backend/manager/cli/__main__.py index 0c479ebb2ae..d582c2d3555 100644 --- a/src/ai/backend/manager/cli/__main__.py +++ b/src/ai/backend/manager/cli/__main__.py @@ -31,7 +31,7 @@ from ai.backend.logging import BraceStyleAdapter, LogLevel from ai.backend.manager.models import error_logs from ai.backend.manager.models.utils import vacuum_db -from ai.backend.manager.raft.utils import register_custom_deserializer +from ai.backend.manager.raft.utils import register_raft_custom_deserializer from .context import CLIContext, redis_ctx @@ -436,7 +436,7 @@ async def handle_raft_cli_main(argv: list[str]): @click.pass_obj @click.argument("args", nargs=-1, type=click.UNPROCESSED) def raft(cli_ctx: CLIContext, args) -> None: - register_custom_deserializer() + register_raft_custom_deserializer() argv = sys.argv # Remove "backend.ai", "mgr", "raft" from the argv diff --git a/src/ai/backend/manager/config.py b/src/ai/backend/manager/config.py index 150cf8a86d6..2008364c960 100644 --- a/src/ai/backend/manager/config.py +++ b/src/ai/backend/manager/config.py @@ -218,6 +218,7 @@ current_resource_slots, ) from ai.backend.logging import BraceStyleAdapter, LogLevel +from ai.backend.manager.api.context import GlobalTimerKind from ai.backend.manager.types import RaftNodeInitialRole from ..manager.defs import INTRINSIC_SLOTS @@ -271,6 +272,9 @@ t.Key("ssl-cert", default=None): t.Null | tx.Path(type="file"), t.Key("ssl-privkey", default=None): t.Null | tx.Path(type="file"), t.Key("event-loop", default="asyncio"): t.Enum("asyncio", "uvloop"), + t.Key("global-timer", default=GlobalTimerKind.DISTRIBUTED_LOCK): tx.Enum( + GlobalTimerKind + ), t.Key("distributed-lock", default="pg_advisory"): t.Enum( "filelock", "pg_advisory", diff --git a/src/ai/backend/manager/idle.py b/src/ai/backend/manager/idle.py index b7a119a9fe8..7708ee5875c 100644 --- a/src/ai/backend/manager/idle.py +++ b/src/ai/backend/manager/idle.py @@ -66,7 +66,7 @@ ) from ai.backend.common.utils import nmget from ai.backend.logging import BraceStyleAdapter -from ai.backend.manager.api.context import RaftClusterContext +from ai.backend.manager.api.context import GlobalTimerContext, GlobalTimerKind from ai.backend.manager.types import DistributedLockFactory from .defs import DEFAULT_ROLE, LockID @@ -193,7 +193,7 @@ def __init__( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, - raft_ctx: RaftClusterContext, + global_timer_ctx: GlobalTimerContext, lock_factory: DistributedLockFactory, ) -> None: self._checkers: list[BaseIdleChecker] = [] @@ -216,7 +216,7 @@ def __init__( self._grace_period_checker: NewUserGracePeriodChecker = NewUserGracePeriodChecker( event_dispatcher, self._redis_live, self._redis_stat ) - self.raft_ctx = raft_ctx + self.global_timer_ctx = global_timer_ctx def add_checker(self, checker: BaseIdleChecker): if self._frozen: @@ -237,22 +237,25 @@ async def start(self) -> None: for checker in self._checkers: await checker.populate_config(raw_config.get(checker.name) or {}) - if self.raft_ctx.use_raft(): - self.timer = RaftGlobalTimer( - self.raft_ctx.raft_node, - self._event_producer, - lambda: DoIdleCheckEvent(), - self.check_interval, - task_name="idle_checker", - ) - else: - self.timer = DistributedLockGlobalTimer( - self._lock_factory(LockID.LOCKID_IDLE_CHECK_TIMER, self.check_interval), - self._event_producer, - lambda: DoIdleCheckEvent(), - self.check_interval, - task_name="idle_checker", - ) + match self.global_timer_ctx.timer_kind: + case GlobalTimerKind.RAFT: + self.timer = RaftGlobalTimer( + self.global_timer_ctx.raft.get_raft_node(), + self._event_producer, + lambda: DoIdleCheckEvent(), + self.check_interval, + task_name="idle_checker", + ) + case GlobalTimerKind.DISTRIBUTED_LOCK: + self.timer = DistributedLockGlobalTimer( + self._lock_factory(LockID.LOCKID_IDLE_CHECK_TIMER, self.check_interval), + self._event_producer, + lambda: DoIdleCheckEvent(), + self.check_interval, + task_name="idle_checker", + ) + case _: + assert False, f"Unknown global timer backend: {self.global_timer_ctx.timer_kind}" self._evh_idle_check = self._event_dispatcher.consume( DoIdleCheckEvent, @@ -1187,7 +1190,7 @@ async def init_idle_checkers( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, - raft_ctx: RaftClusterContext, + global_timer_ctx: GlobalTimerContext, lock_factory: DistributedLockFactory, ) -> IdleCheckerHost: """ @@ -1199,7 +1202,7 @@ async def init_idle_checkers( shared_config, event_dispatcher, event_producer, - raft_ctx, + global_timer_ctx, lock_factory, ) checker_init_args = (event_dispatcher, checker_host._redis_live, checker_host._redis_stat) diff --git a/src/ai/backend/manager/raft/utils.py b/src/ai/backend/manager/raft/utils.py index 7a0ac5c640b..f430e1e9240 100644 --- a/src/ai/backend/manager/raft/utils.py +++ b/src/ai/backend/manager/raft/utils.py @@ -93,7 +93,7 @@ def pickle_deserialize(data: bytes) -> str | None: return None -def register_custom_deserializer() -> None: +def register_raft_custom_deserializer() -> None: """ Initialize the custom deserializers. """ diff --git a/src/ai/backend/manager/scheduler/dispatcher.py b/src/ai/backend/manager/scheduler/dispatcher.py index a6e28619f68..8f2e25260b8 100644 --- a/src/ai/backend/manager/scheduler/dispatcher.py +++ b/src/ai/backend/manager/scheduler/dispatcher.py @@ -67,7 +67,7 @@ aobject, ) from ai.backend.logging import BraceStyleAdapter -from ai.backend.manager.api.context import RaftClusterContext +from ai.backend.manager.api.context import GlobalTimerContext, GlobalTimerKind from ai.backend.manager.defs import SERVICE_MAX_RETRIES, LockID from ai.backend.manager.models.agent import AgentRow from ai.backend.manager.models.session import _build_session_fetch_query @@ -81,7 +81,6 @@ SessionNotFound, ) from ..defs import SERVICE_MAX_RETRIES, LockID -from ..api.exceptions import GenericBadRequest, InstanceNotAvailable, SessionNotFound from ..exceptions import convert_to_status_data from ..models import ( AgentStatus, @@ -180,7 +179,7 @@ class SchedulerDispatcher(aobject): def __init__( self, - raft_ctx: RaftClusterContext, + global_timer_ctx: GlobalTimerContext, local_config: LocalConfig, shared_config: SharedConfig, event_dispatcher: EventDispatcher, @@ -188,7 +187,7 @@ def __init__( lock_factory: DistributedLockFactory, registry: AgentRegistry, ) -> None: - self.raft_ctx = raft_ctx + self.global_timer_ctx = global_timer_ctx self.local_config = local_config self.shared_config = shared_config self.event_dispatcher = event_dispatcher @@ -220,54 +219,57 @@ async def __ainit__(self) -> None: evd.consume(DoPrepareEvent, None, self.prepare) evd.consume(DoScaleEvent, None, self.scale_services) - if self.raft_ctx.use_raft(): - self.schedule_timer = RaftGlobalTimer( - self.raft_ctx.raft_node, - self.event_producer, - lambda: DoScheduleEvent(), - interval=10.0, - task_name="schedule_timer", - ) - self.prepare_timer = RaftGlobalTimer( - self.raft_ctx.raft_node, - self.event_producer, - lambda: DoPrepareEvent(), - interval=10.0, - initial_delay=5.0, - task_name="prepare_timer", - ) - self.scale_timer = RaftGlobalTimer( - self.raft_ctx.raft_node, - self.event_producer, - lambda: DoScaleEvent(), - interval=10.0, - initial_delay=7.0, - task_name="scale_timer", - ) - else: - self.schedule_timer = DistributedLockGlobalTimer( - self.lock_factory(LockID.LOCKID_SCHEDULE_TIMER, 10.0), - self.event_producer, - lambda: DoScheduleEvent(), - interval=10.0, - task_name="schedule_timer", - ) - self.prepare_timer = DistributedLockGlobalTimer( - self.lock_factory(LockID.LOCKID_PREPARE_TIMER, 10.0), - self.event_producer, - lambda: DoPrepareEvent(), - interval=10.0, - initial_delay=5.0, - task_name="prepare_timer", - ) - self.scale_timer = DistributedLockGlobalTimer( - self.lock_factory(LockID.LOCKID_SCALE_TIMER, 10.0), - self.event_producer, - lambda: DoScaleEvent(), - interval=10.0, - initial_delay=7.0, - task_name="scale_timer", - ) + match self.global_timer_ctx.timer_kind: + case GlobalTimerKind.RAFT: + self.schedule_timer = RaftGlobalTimer( + self.global_timer_ctx.raft.get_raft_node(), + self.event_producer, + lambda: DoScheduleEvent(), + interval=10.0, + task_name="schedule_timer", + ) + self.prepare_timer = RaftGlobalTimer( + self.global_timer_ctx.raft.get_raft_node(), + self.event_producer, + lambda: DoPrepareEvent(), + interval=10.0, + initial_delay=5.0, + task_name="prepare_timer", + ) + self.scale_timer = RaftGlobalTimer( + self.global_timer_ctx.raft.get_raft_node(), + self.event_producer, + lambda: DoScaleEvent(), + interval=10.0, + initial_delay=7.0, + task_name="scale_timer", + ) + case GlobalTimerKind.DISTRIBUTED_LOCK: + self.schedule_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_SCHEDULE_TIMER, 10.0), + self.event_producer, + lambda: DoScheduleEvent(), + interval=10.0, + task_name="schedule_timer", + ) + self.prepare_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_PREPARE_TIMER, 10.0), + self.event_producer, + lambda: DoPrepareEvent(), + interval=10.0, + initial_delay=5.0, + task_name="prepare_timer", + ) + self.scale_timer = DistributedLockGlobalTimer( + self.lock_factory(LockID.LOCKID_SCALE_TIMER, 10.0), + self.event_producer, + lambda: DoScaleEvent(), + interval=10.0, + initial_delay=7.0, + task_name="scale_timer", + ) + case _: + assert False, f"Unknown global timer backend: {self.global_timer_ctx.timer_kind}" await self.schedule_timer.join() await self.prepare_timer.join() diff --git a/src/ai/backend/manager/server.py b/src/ai/backend/manager/server.py index 2bf416e1467..7e70e6a6f1e 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -59,12 +59,12 @@ from ai.backend.logging import BraceStyleAdapter, Logger, LogLevel from ai.backend.manager.raft.logger import Logger as RaftLogger from ai.backend.manager.raft.state_machine import HashStore -from ai.backend.manager.raft.utils import WebServer, register_custom_deserializer +from ai.backend.manager.raft.utils import WebServer, register_raft_custom_deserializer from . import __version__ from .agent_cache import AgentRPCCache from .api import ManagerStatus -from .api.context import RaftClusterContext, RootContext +from .api.context import GlobalTimerContext, GlobalTimerKind, RootContext from .api.exceptions import ( BackendError, GenericBadRequest, @@ -440,7 +440,7 @@ async def idle_checker_ctx(root_ctx: RootContext) -> AsyncIterator[None]: root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) await root_ctx.idle_checker_host.start() @@ -519,7 +519,7 @@ async def sched_dispatcher_ctx(root_ctx: RootContext) -> AsyncIterator[None]: from .scheduler.dispatcher import SchedulerDispatcher sched_dispatcher = await SchedulerDispatcher.new( - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.local_config, root_ctx.shared_config, root_ctx.event_dispatcher, @@ -668,11 +668,13 @@ async def _force_terminate_hanging_sessions( @actxmgr async def raft_ctx(root_ctx: RootContext) -> AsyncIterator[None]: - register_custom_deserializer() - raft_configs = root_ctx.local_config.get("raft") - raft_cluster_configs = root_ctx.raft_cluster_config + if root_ctx.global_timer_ctx.timer_kind == GlobalTimerKind.RAFT: + register_raft_custom_deserializer() - if raft_configs is not None: + raft_configs = root_ctx.local_config.get("raft") + assert raft_configs is not None, "Raft configuration missing in the manager.toml" + + raft_cluster_configs = root_ctx.raft_cluster_config assert raft_cluster_configs is not None other_peers = [{**peer, "myself": False} for peer in raft_cluster_configs["peers"]["other"]] @@ -729,14 +731,14 @@ async def raft_ctx(root_ctx: RootContext) -> AsyncIterator[None]: logging.getLogger(f"{__spec__.name}.raft.node-{node_id}"), # type: ignore ) - root_ctx.raft_ctx.cluster = Raft.bootstrap( + root_ctx.global_timer_ctx.raft = Raft.bootstrap( node_id, raft_addr, store, # type: ignore raft_cfg, raft_logger, # type: ignore ) - raft_cluster = root_ctx.raft_ctx.cluster + raft_cluster = root_ctx.global_timer_ctx.raft raft_cluster.run() # type: ignore if raft_cluster_configs["raft-debug-webserver-enabled"]: @@ -975,7 +977,9 @@ async def server_main( ) -> AsyncIterator[None]: root_app = build_root_app(pidx, _args[0], _args[1], subapp_pkgs=global_subapp_pkgs) root_ctx: RootContext = root_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext( + root_ctx.local_config["manager"].get("global-timer") + ) # Start aiomonitor. # Port is set by config (default=50100 + pidx). diff --git a/tests/manager/test_idle_checker.py b/tests/manager/test_idle_checker.py index b1a03f34432..8f97207346b 100644 --- a/tests/manager/test_idle_checker.py +++ b/tests/manager/test_idle_checker.py @@ -7,7 +7,7 @@ from ai.backend.common import msgpack, redis_helper from ai.backend.common.types import KernelId, SessionId, SessionTypes -from ai.backend.manager.api.context import RaftClusterContext, RootContext +from ai.backend.manager.api.context import GlobalTimerContext, GlobalTimerKind, RootContext from ai.backend.manager.idle import ( BaseIdleChecker, IdleCheckerHost, @@ -97,7 +97,7 @@ async def new_user_grace_period_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) # test config grace_period = 30 @@ -117,7 +117,7 @@ async def new_user_grace_period_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -148,7 +148,7 @@ async def network_timeout_idle_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) # test 1 # remaining time is positive and no grace period @@ -180,7 +180,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -234,7 +234,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -292,7 +292,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -355,7 +355,7 @@ async def network_timeout_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -404,7 +404,7 @@ async def session_lifetime_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) # test 1 # remaining time is positive and no grace period @@ -432,7 +432,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -480,7 +480,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -533,7 +533,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -588,7 +588,7 @@ async def session_lifetime_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) try: @@ -632,7 +632,7 @@ async def utilization_idle_checker__utilization( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) kernel_id = KernelId(uuid4()) expected = { @@ -678,7 +678,7 @@ async def utilization_idle_checker__utilization( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -718,7 +718,7 @@ async def utilization_idle_checker( [".etcd"], ) root_ctx: RootContext = test_app["_root.context"] - root_ctx.raft_ctx = RaftClusterContext() + root_ctx.global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) # test 1 # remaining time is positive and no utilization. @@ -779,7 +779,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -864,7 +864,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( @@ -949,7 +949,7 @@ async def utilization_idle_checker( root_ctx.shared_config, root_ctx.event_dispatcher, root_ctx.event_producer, - root_ctx.raft_ctx, + root_ctx.global_timer_ctx, root_ctx.distributed_lock_factory, ) await redis_helper.execute( diff --git a/tests/manager/test_scheduler.py b/tests/manager/test_scheduler.py index d625c966147..830f6cab6cc 100644 --- a/tests/manager/test_scheduler.py +++ b/tests/manager/test_scheduler.py @@ -26,7 +26,7 @@ SessionId, SessionTypes, ) -from ai.backend.manager.api.context import RaftClusterContext +from ai.backend.manager.api.context import GlobalTimerContext, GlobalTimerKind from ai.backend.manager.defs import DEFAULT_ROLE from ai.backend.manager.models.agent import AgentRow from ai.backend.manager.models.image import ImageRow @@ -1106,7 +1106,7 @@ async def test_manually_assign_agent_available( candidate_agents = example_agents example_pending_sessions[0].kernels[0].agent = example_agents[0].id sess_ctx = example_pending_sessions[0] - raft_ctx = RaftClusterContext() + global_timer_ctx = GlobalTimerContext(GlobalTimerKind.DISTRIBUTED_LOCK) dispatcher = SchedulerDispatcher( local_config=mock_local_config, @@ -1114,7 +1114,7 @@ async def test_manually_assign_agent_available( event_dispatcher=mock_event_dispatcher, event_producer=mock_event_producer, lock_factory=file_lock_factory, - raft_ctx=raft_ctx, + global_timer_ctx=global_timer_ctx, registry=registry, ) From be8d5d6baf250152b3281fd91098cd01e5d4ccfe Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 10 May 2024 06:34:17 +0000 Subject: [PATCH 5/7] refactor: Use `enum.StrEnum` --- src/ai/backend/manager/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/backend/manager/types.py b/src/ai/backend/manager/types.py index a73d9ef847b..6fa5953a7bb 100644 --- a/src/ai/backend/manager/types.py +++ b/src/ai/backend/manager/types.py @@ -64,7 +64,7 @@ class RaftNodeInitialRole(str, enum.Enum): LEARNER = "learner" -class RaftLogLovel(str, enum.Enum): +class RaftLogLovel(enum.StrEnum): TRACE = "trace" DEBUG = "debug" INFO = "info" From dd0141eee91fe49b9760f9a6174bd8a29bec6fc0 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 10 May 2024 06:50:46 +0000 Subject: [PATCH 6/7] fix: typo --- src/ai/backend/manager/api/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/backend/manager/api/context.py b/src/ai/backend/manager/api/context.py index a53ce3bf7b4..fb5aeaa6a97 100644 --- a/src/ai/backend/manager/api/context.py +++ b/src/ai/backend/manager/api/context.py @@ -30,7 +30,7 @@ class BaseContext: class GlobalTimerKind(enum.StrEnum): RAFT = "raft" - DISTRIBUTED_LOCK = "distributed-lock" + DISTRIBUTED_LOCK = "distributed_lock" class GlobalTimerContext: From a58560115673cc8af509dcf48fe960b0623b4535 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 2 Sep 2024 07:27:18 +0000 Subject: [PATCH 7/7] fix: Merge with main branch --- python.lock | 826 +++++++++--------- requirements.txt | 2 +- src/ai/backend/manager/api/context.py | 8 +- src/ai/backend/manager/api/logs.py | 1 - src/ai/backend/manager/idle.py | 4 +- .../backend/manager/scheduler/dispatcher.py | 7 +- src/ai/backend/manager/types.py | 23 +- 7 files changed, 413 insertions(+), 458 deletions(-) diff --git a/python.lock b/python.lock index e9b39f2a288..89611e4f04b 100644 --- a/python.lock +++ b/python.lock @@ -76,7 +76,7 @@ // "python-dotenv~=0.20.0", // "python-json-logger>=2.0.1", // "pyzmq~=25.1.2", -// "raftify==0.1.65", +// "raftify==0.1.67", // "redis[hiredis]==4.5.5", // "rich~=13.6", // "setproctitle~=1.3.2", @@ -126,19 +126,25 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1867a7cc86897a87398e6e6fba302738548f1cf76cbc6c76e06338e091113bdc", - "url": "https://files.pythonhosted.org/packages/08/17/98008117ec3f484259f11a8a96cb5601949546a4de43102b99cffa1138e5/aioconsole-0.7.1-py3-none-any.whl" + "hash": "a8f7d3049df0518f4e50de7d94c082097785b6a675befd62e8da5539d3d2f8ca", + "url": "https://files.pythonhosted.org/packages/c4/4d/4bd0c11f58dfdd2155f82f461d02646e9ab759d902c4a62c935e60cc5ea8/aioconsole-0.8.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "a3e52428d32623c96746ec3862d97483c61c12a2f2dfba618886b709415d4533", - "url": "https://files.pythonhosted.org/packages/e5/f4/f156826819b4136b3fe9fac1b7707f6f241c871aaef13b4a16932e39156d/aioconsole-0.7.1.tar.gz" + "hash": "64c93c17ef878fc68b4b379b9ed7fbb96c6fbc1b4e9a8378f9f899299ebdd37f", + "url": "https://files.pythonhosted.org/packages/89/dc/523222a45a83e69319724362db1664185970bca20c7d643c9261cfcddfb1/aioconsole-0.8.0.tar.gz" } ], "project_name": "aioconsole", - "requires_dists": [], + "requires_dists": [ + "pytest-asyncio; extra == \"dev\"", + "pytest-cov; extra == \"dev\"", + "pytest-repeat; extra == \"dev\"", + "pytest; extra == \"dev\"", + "uvloop; (platform_python_implementation != \"PyPy\" and sys_platform != \"win32\") and extra == \"dev\"" + ], "requires_python": ">=3.8", - "version": "0.7.1" + "version": "0.8.0" }, { "artifacts": [ @@ -761,42 +767,61 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", - "url": "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl" + "hash": "81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", + "url": "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "url": "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz" + "hash": "5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "url": "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz" } ], "project_name": "attrs", "requires_dists": [ - "attrs[tests-mypy]; extra == \"tests-no-zope\"", - "attrs[tests-no-zope]; extra == \"tests\"", - "attrs[tests]; extra == \"cov\"", - "attrs[tests]; extra == \"dev\"", - "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"tests-no-zope\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"benchmark\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"cov\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"dev\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"tests\"", + "cogapp; extra == \"docs\"", "coverage[toml]>=5.3; extra == \"cov\"", "furo; extra == \"docs\"", - "hypothesis; extra == \"tests-no-zope\"", + "hypothesis; extra == \"benchmark\"", + "hypothesis; extra == \"cov\"", + "hypothesis; extra == \"dev\"", + "hypothesis; extra == \"tests\"", "importlib-metadata; python_version < \"3.8\"", - "mypy>=1.6; (platform_python_implementation == \"CPython\" and python_version >= \"3.8\") and extra == \"tests-mypy\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"benchmark\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"cov\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"dev\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests-mypy\"", "myst-parser; extra == \"docs\"", "pre-commit; extra == \"dev\"", - "pympler; extra == \"tests-no-zope\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.8\") and extra == \"tests-mypy\"", - "pytest-xdist[psutil]; extra == \"tests-no-zope\"", - "pytest>=4.3.0; extra == \"tests-no-zope\"", + "pympler; extra == \"benchmark\"", + "pympler; extra == \"cov\"", + "pympler; extra == \"dev\"", + "pympler; extra == \"tests\"", + "pytest-codspeed; extra == \"benchmark\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"benchmark\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"cov\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"dev\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests-mypy\"", + "pytest-xdist[psutil]; extra == \"benchmark\"", + "pytest-xdist[psutil]; extra == \"cov\"", + "pytest-xdist[psutil]; extra == \"dev\"", + "pytest-xdist[psutil]; extra == \"tests\"", + "pytest>=4.3.0; extra == \"benchmark\"", + "pytest>=4.3.0; extra == \"cov\"", + "pytest>=4.3.0; extra == \"dev\"", + "pytest>=4.3.0; extra == \"tests\"", "sphinx-notfound-page; extra == \"docs\"", "sphinx; extra == \"docs\"", "sphinxcontrib-towncrier; extra == \"docs\"", - "towncrier; extra == \"docs\"", - "zope-interface; extra == \"docs\"", - "zope-interface; extra == \"tests\"" + "towncrier<24.7; extra == \"docs\"" ], "requires_python": ">=3.7", - "version": "23.2.0" + "version": "24.2.0" }, { "artifacts": [ @@ -861,98 +886,98 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "6717543d2c110a155e6821ce5670c1f512f602eabb77dba95717ca76af79867d", - "url": "https://files.pythonhosted.org/packages/9c/64/a016d23b6f513282d8b7f9dd91342929a2e970b2e2c2576d9b76f8f2ee5a/bcrypt-4.1.3-cp39-abi3-musllinux_1_2_x86_64.whl" + "hash": "cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", + "url": "https://files.pythonhosted.org/packages/1a/d4/586b9c18a327561ea4cd336ff4586cca1a7aa0f5ee04e23a8a8bb9ca64f1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8cbb119267068c2581ae38790e0d1fbae65d0725247a930fc9900c285d95725d", - "url": "https://files.pythonhosted.org/packages/0f/e8/183ead5dd8124e463d0946dfaf86c658225adde036aede8384d21d1794d0/bcrypt-4.1.3-cp37-abi3-musllinux_1_1_x86_64.whl" + "hash": "3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", + "url": "https://files.pythonhosted.org/packages/00/03/2af7c45034aba6002d4f2b728c1a385676b4eab7d764410e34fd768009f2/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ec3c2e1ca3e5c4b9edb94290b356d082b721f3f50758bce7cce11d8a7c89ce84", - "url": "https://files.pythonhosted.org/packages/12/d4/13b86b1bb2969a804c2347d0ad72fc3d3d9f5cf0d876c84451c6480e19bc/bcrypt-4.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", + "url": "https://files.pythonhosted.org/packages/05/d2/1be1e16aedec04bcf8d0156e01b987d16a2063d38e64c3f28030a3427d61/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "551b320396e1d05e49cc18dd77d970accd52b322441628aca04801bbd1d52a73", - "url": "https://files.pythonhosted.org/packages/23/85/283450ee672719e216a5e1b0e80cb0c8f225bc0814cbb893155ee4fdbb9e/bcrypt-4.1.3-cp39-abi3-musllinux_1_2_aarch64.whl" + "hash": "3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", + "url": "https://files.pythonhosted.org/packages/3e/d0/31938bb697600a04864246acde4918c4190a938f891fd11883eaaf41327a/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "3a5be252fef513363fe281bafc596c31b552cf81d04c5085bc5dac29670faa08", - "url": "https://files.pythonhosted.org/packages/29/3c/6e478265f68eff764571676c0773086d15378fdf5347ddf53e5025c8b956/bcrypt-4.1.3-cp39-abi3-manylinux_2_28_aarch64.whl" + "hash": "1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", + "url": "https://files.pythonhosted.org/packages/46/54/dc7b58abeb4a3d95bab653405935e27ba32f21b812d8ff38f271fb6f7f55/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "31adb9cbb8737a581a843e13df22ffb7c84638342de3708a98d5c986770f2834", - "url": "https://files.pythonhosted.org/packages/2c/fd/0d2d7cc6fc816010f6c6273b778e2f147e2eca1144975b6b71e344b26ca0/bcrypt-4.1.3-cp39-abi3-musllinux_1_1_x86_64.whl" + "hash": "3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", + "url": "https://files.pythonhosted.org/packages/4b/3b/ad784eac415937c53da48983756105d267b91e56aa53ba8a1b2014b8d930/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6cac78a8d42f9d120b3987f82252bdbeb7e6e900a5e1ba37f6be6fe4e3848286", - "url": "https://files.pythonhosted.org/packages/2d/5e/edcb4ec57b056ca9d5f9fde31fcda10cc635def48867edff5cc09a348a4f/bcrypt-4.1.3-cp37-abi3-musllinux_1_2_aarch64.whl" + "hash": "27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", + "url": "https://files.pythonhosted.org/packages/5d/2c/019bc2c63c6125ddf0483ee7d914a405860327767d437913942b476e9c9b/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "3d3b317050a9a711a5c7214bf04e28333cf528e0ed0ec9a4e55ba628d0f07c1a", - "url": "https://files.pythonhosted.org/packages/2f/f6/9c0a6de7ef78d573e10d0b7de3ef82454e2e6eb6fada453ea6c2b8fb3f0a/bcrypt-4.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", + "url": "https://files.pythonhosted.org/packages/75/fe/9e137727f122bbe29771d56afbf4e0dbc85968caa8957806f86404a5bfe1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "01746eb2c4299dd0ae1670234bf77704f581dd72cc180f444bfe74eb80495b64", - "url": "https://files.pythonhosted.org/packages/3b/5d/121130cc85009070fe4e4f5937b213a00db143147bc6c8677b3fd03deec8/bcrypt-4.1.3-cp37-abi3-musllinux_1_2_x86_64.whl" + "hash": "0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", + "url": "https://files.pythonhosted.org/packages/7b/76/2aa660679abbdc7f8ee961552e4bb6415a81b303e55e9374533f22770203/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4fb253d65da30d9269e0a6f4b0de32bd657a0208a6f4e43d3e645774fb5457f3", - "url": "https://files.pythonhosted.org/packages/4c/6a/ce950d4350c734bc5d9b7196a58fedbdc94f564c00b495a1222984431e03/bcrypt-4.1.3-cp37-abi3-manylinux_2_28_x86_64.whl" + "hash": "c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", + "url": "https://files.pythonhosted.org/packages/96/86/8c6a84daed4dd878fbab094400c9174c43d9b838ace077a2f8ee8bc3ae12/bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "094fd31e08c2b102a14880ee5b3d09913ecf334cd604af27e1013c76831f7b05", - "url": "https://files.pythonhosted.org/packages/63/56/45312e49c195cd30e1bf4b7f0e039e8b3c46802cd55485947ddcb96caa27/bcrypt-4.1.3-cp37-abi3-manylinux_2_28_aarch64.whl" + "hash": "096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", + "url": "https://files.pythonhosted.org/packages/a9/81/4e8f5bc0cd947e91fb720e1737371922854da47a94bc9630454e7b2845f8/bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455", - "url": "https://files.pythonhosted.org/packages/7c/8d/ad2efe0ec57ed3c25e588c4543d946a1c72f8ee357a121c0e382d8aaa93f/bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", + "url": "https://files.pythonhosted.org/packages/ac/be/da233c5f11fce3f8adec05e8e532b299b64833cc962f49331cdd0e614fa9/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "5f7cd3399fbc4ec290378b541b0cf3d4398e4737a65d0f938c7c0f9d5e686611", - "url": "https://files.pythonhosted.org/packages/97/00/21e34b365b895e6faf9cc5d4e7b97dd419e08f8a7df119792ec206b4a3fa/bcrypt-4.1.3-cp39-abi3-manylinux_2_28_x86_64.whl" + "hash": "1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", + "url": "https://files.pythonhosted.org/packages/b0/b8/8b4add88d55a263cf1c6b8cf66c735280954a04223fcd2880120cc767ac3/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "f5698ce5292a4e4b9e5861f7e53b1d89242ad39d54c3da451a93cac17b61921a", - "url": "https://files.pythonhosted.org/packages/a4/9a/4aa31d1de9369737cfa734a60c3d125ecbd1b3ae2c6499986d0ac160ea8b/bcrypt-4.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", + "url": "https://files.pythonhosted.org/packages/cc/14/b9ff8e0218bee95e517b70e91130effb4511e8827ac1ab00b4e30943a3f6/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "0d4cf6ef1525f79255ef048b3489602868c47aea61f375377f0d00514fe4a78c", - "url": "https://files.pythonhosted.org/packages/a8/eb/fbea8d2b370a4cc7f5f0aff9f492177a5813e130edeab9dd388ddd3ef1dc/bcrypt-4.1.3-cp39-abi3-macosx_10_12_universal2.whl" + "hash": "762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", + "url": "https://files.pythonhosted.org/packages/dc/5d/6843443ce4ab3af40bddb6c7c085ed4a8418b3396f7a17e60e6d9888416c/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "193bb49eeeb9c1e2db9ba65d09dc6384edd5608d9d672b4125e9320af9153a15", - "url": "https://files.pythonhosted.org/packages/af/a1/36aa84027ef45558b30a485bc5b0606d5e7357b27b10cc49dce3944f4d1d/bcrypt-4.1.3-cp37-abi3-musllinux_1_1_aarch64.whl" + "hash": "1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", + "url": "https://files.pythonhosted.org/packages/e3/96/7a654027638ad9b7589effb6db77eb63eba64319dfeaf9c0f4ca953e5f76/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2ee15dd749f5952fe3f0430d0ff6b74082e159c50332a1413d51b5689cf06623", - "url": "https://files.pythonhosted.org/packages/ca/e9/0b36987abbcd8c9210c7b86673d88ff0a481b4610630710fb80ba5661356/bcrypt-4.1.3.tar.gz" + "hash": "cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", + "url": "https://files.pythonhosted.org/packages/e4/7e/d95e7d96d4828e965891af92e43b52a4cd3395dc1c1ef4ee62748d0471d0/bcrypt-4.2.0.tar.gz" }, { "algorithm": "sha256", - "hash": "c4c8d9b3e97209dd7111bf726e79f638ad9224b4691d1c7cfefa571a09b1b2d6", - "url": "https://files.pythonhosted.org/packages/e0/c9/069b0c3683ce969b328b7b3e3218f9d5981d0629f6091b3b1dfa72928f75/bcrypt-4.1.3-cp39-abi3-musllinux_1_1_aarch64.whl" + "hash": "9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", + "url": "https://files.pythonhosted.org/packages/e7/c3/dae866739989e3f04ae304e1201932571708cb292a28b2f1b93283e2dcd8/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74", - "url": "https://files.pythonhosted.org/packages/fe/4e/e424a74f0749998d8465c162c5cb9d9f210a5b60444f4120eff0af3fa800/bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl" + "hash": "3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", + "url": "https://files.pythonhosted.org/packages/f6/05/e394515f4e23c17662e5aeb4d1859b11dc651be01a3bd03c2e919a155901/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" } ], "project_name": "bcrypt", @@ -961,7 +986,7 @@ "pytest!=3.3.0,>=3.2.1; extra == \"tests\"" ], "requires_python": ">=3.7", - "version": "4.1.3" + "version": "4.2.0" }, { "artifacts": [ @@ -991,80 +1016,48 @@ "artifacts": [ { "algorithm": "sha256", -<<<<<<< HEAD - "hash": "b8433d481d50b68a0162c0379c0dd4aabfc3d1ad901800beb5b87815997511c1", - "url": "https://files.pythonhosted.org/packages/9a/b0/a4301290ea6cdbb0cda7048ae11b0e560eacca7d2c2e64e6b3d5a9fb3fde/boto3-1.34.144-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2f3e88b10b8fcc5f6100a9d74cd28230edc9d4fa226d99dd40a3ab38ac213673", - "url": "https://files.pythonhosted.org/packages/42/e5/738f7bf96f4f5597c8393e11be2c28bef5f876b5635c1ea9d86888e59657/boto3-1.34.144.tar.gz" -======= - "hash": "6c8125310005255ea998bccc3e8353b4df81a96ab105c89c118461f6c54c07c8", - "url": "https://files.pythonhosted.org/packages/09/18/6b9e0bbdc28a11c1f953160934cd10c938811345d80c3d9c5719c18fe522/boto3-1.34.97-py3-none-any.whl" + "hash": "add26dd58e076dfd387013da4704716d5cff215cf14f6d4347c4b9b7fc1f0b8e", + "url": "https://files.pythonhosted.org/packages/63/c2/f32fddf5a40456fb0564a32d5d20b37b8ad00c3fe6122aab602be139e459/boto3-1.35.10-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "60e5dda0b29805fb410bfda1d98e898edaebedac0e6983e9c57cb88e44dfa64e", - "url": "https://files.pythonhosted.org/packages/e5/27/9073116821d6cf73d6463424e3f2d3ab0edaf2ee182c9eb1b263defa3eaf/boto3-1.34.97.tar.gz" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "hash": "189ab1e2b4cd86df56f82438d89b4040eb140c92683f1bda7cb2e62624f20ea5", + "url": "https://files.pythonhosted.org/packages/be/01/3e6dce0f2364f1814f735a01c52c3c55d87c079daefeb6569ab7829c2520/boto3-1.35.10.tar.gz" } ], "project_name": "boto3", "requires_dists": [ -<<<<<<< HEAD - "botocore<1.35.0,>=1.34.144", -======= - "botocore<1.35.0,>=1.34.97", ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "botocore<1.36.0,>=1.35.10", "botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "s3transfer<0.11.0,>=0.10.0" ], "requires_python": ">=3.8", -<<<<<<< HEAD - "version": "1.34.144" -======= - "version": "1.34.97" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "version": "1.35.10" }, { "artifacts": [ { "algorithm": "sha256", -<<<<<<< HEAD - "hash": "a2cf26e1bf10d5917a2285e50257bc44e94a1d16574f282f3274f7a5d8d1f08b", - "url": "https://files.pythonhosted.org/packages/24/4b/956a80d406dfffba1f8f7fbaba7dd73d418ed8a7b95faa1ade7cf17663a5/botocore-1.34.144-py3-none-any.whl" + "hash": "0d96d023b9b0cea99a0a428a431d011329d3a958730aee6ed6a6fec5d9bfbc03", + "url": "https://files.pythonhosted.org/packages/f4/1d/2265ef470c95ebf0250f442e17f2ebd80113312c715c877e9161816aa8e8/botocore-1.35.10-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4215db28d25309d59c99507f1f77df9089e5bebbad35f6e19c7c44ec5383a3e8", - "url": "https://files.pythonhosted.org/packages/8c/66/01d63edf404b2ef2c5594701565ac0c031ce7253231298d423e2514566b8/botocore-1.34.144.tar.gz" -======= - "hash": "c98b1272e377c69e167cc68c0f2c9c79bc7a6098775eecdad41ee5a28de69324", - "url": "https://files.pythonhosted.org/packages/79/2a/cf6ad65939b48a4a3a984174928abab0955e4627a79bf552c876dd17172b/botocore-1.34.97-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e421b592add68547ed141643c8a8b4aa819a07059b85efd72e89b6758c956420", - "url": "https://files.pythonhosted.org/packages/8e/4d/724915591564af6c31bf6a0a41981542634e3b8000a0a1aa61541719c5b1/botocore-1.34.97.tar.gz" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "hash": "6c8a1377b6636a0d80218115e1cd41bcceba0a2f050b79c206f4cf8d002c54d7", + "url": "https://files.pythonhosted.org/packages/71/76/fd28cb2b1ab3b19b0f2602455c1757a0517fededd51356737120e6a29ce8/botocore-1.35.10.tar.gz" } ], "project_name": "botocore", "requires_dists": [ - "awscrt==0.20.11; extra == \"crt\"", + "awscrt==0.21.2; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "python-dateutil<3.0.0,>=2.1", "urllib3!=2.2.0,<3,>=1.25.4; python_version >= \"3.10\"", "urllib3<1.27,>=1.25.4; python_version < \"3.10\"" ], "requires_python": ">=3.8", -<<<<<<< HEAD - "version": "1.34.144" -======= - "version": "1.34.97" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "version": "1.35.10" }, { "artifacts": [ @@ -1162,66 +1155,71 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", - "url": "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl" + "hash": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "url": "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "url": "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz" + "hash": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", + "url": "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz" } ], "project_name": "certifi", "requires_dists": [], "requires_python": ">=3.6", - "version": "2024.7.4" + "version": "2024.8.30" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "url": "https://files.pythonhosted.org/packages/4c/00/e17e2a8df0ff5aca2edd9eeebd93e095dd2515f2dd8d591d84a3233518f6/cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", + "url": "https://files.pythonhosted.org/packages/fc/83/8353e5c9b01bb46332dac3dfb18e6c597a04ceb085c19c814c2f78a8c0d0/cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "url": "https://files.pythonhosted.org/packages/09/d4/8759cc3b2222c159add8ce3af0089912203a31610f4be4c36f98e320b4c6/cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", + "url": "https://files.pythonhosted.org/packages/00/13/150924609bf377140abe6e934ce0a57f3fc48f1fd956ec1f578ce97a4624/cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "url": "https://files.pythonhosted.org/packages/22/04/1d10d5baf3faaae9b35f6c49bcf25c1be81ea68cc7ee6923206d02be85b0/cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", + "url": "https://files.pythonhosted.org/packages/0f/7c/a6beb119ad515058c5ee1829742d96b25b2b9204ff920746f6e13bf574eb/cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "url": "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz" + "hash": "c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", + "url": "https://files.pythonhosted.org/packages/17/fd/7d73d7110155c036303b0a6462c56250e9bc2f4119d7591d27417329b4d1/cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "url": "https://files.pythonhosted.org/packages/9b/1a/575200306a3dfd9102ce573e7158d459a1bd7e44637e4f22a999c4fd64b1/cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", + "url": "https://files.pythonhosted.org/packages/1a/1f/7862231350cc959a3138889d2c8d33da7042b22e923457dfd4cd487d772a/cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "url": "https://files.pythonhosted.org/packages/a3/81/5f5d61338951afa82ce4f0f777518708893b9420a8b309cc037fbf114e63/cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", + "url": "https://files.pythonhosted.org/packages/1e/bf/82c351342972702867359cfeba5693927efe0a8dd568165490144f554b18/cffi-1.17.0.tar.gz" }, { "algorithm": "sha256", - "hash": "fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357", - "url": "https://files.pythonhosted.org/packages/b4/5f/c6e7e8d80fbf727909e4b1b5b9352082fc1604a14991b1d536bfaee5a36c/cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", + "url": "https://files.pythonhosted.org/packages/61/8a/2575cd01a90e1eca96a30aec4b1ac101a6fae06c49d490ac2704fa9bc8ba/cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "url": "https://files.pythonhosted.org/packages/b4/f6/b28d2bfb5fca9e8f9afc9d05eae245bed9f6ba5c2897fefee7a9abeaf091/cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", + "url": "https://files.pythonhosted.org/packages/61/94/4882c47d3ad396d91f0eda6ef16d45be3d752a332663b7361933039ed66a/cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "url": "https://files.pythonhosted.org/packages/e4/c7/c09cc6fd1828ea950e60d44e0ef5ed0b7e3396fbfb856e49ca7d629b1408/cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", + "url": "https://files.pythonhosted.org/packages/8b/8c/26119bf8b79e05a1c39812064e1ee7981e1f8a5372205ba5698ea4dd958d/cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", + "url": "https://files.pythonhosted.org/packages/cd/66/85899f5a9f152db49646e0c77427173e1b77a1046de0191ab3b0b9a5e6e3/cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" } ], "project_name": "cffi", @@ -1229,7 +1227,7 @@ "pycparser" ], "requires_python": ">=3.8", - "version": "1.16.0" + "version": "1.17.0" }, { "artifacts": [ @@ -1378,103 +1376,78 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", - "url": "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", - "url": "https://files.pythonhosted.org/packages/07/40/d6f6819c62e808ea74639c3c640f7edd636b86cce62cb14943996a15df92/cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", - "url": "https://files.pythonhosted.org/packages/0f/38/85c74d0ac4c540780e072b1e6f148ecb718418c1062edcb20d22f3ec5bbb/cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", - "url": "https://files.pythonhosted.org/packages/25/c9/86f04e150c5d5d5e4a731a2c1e0e43da84d901f388e3fea3d5de98d689a7/cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", - "url": "https://files.pythonhosted.org/packages/35/66/2d87e9ca95c82c7ee5f2c09716fc4c4242c1ae6647b9bd27e55e920e9f10/cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", - "url": "https://files.pythonhosted.org/packages/43/c2/4a3eef67e009a522711ebd8ac89424c3a7fe591ece7035d964419ad52a1d/cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", + "url": "https://files.pythonhosted.org/packages/bd/f6/e4387edb55563e2546028ba4c634522fe727693d3cdd9ec0ecacedc75411/cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", - "url": "https://files.pythonhosted.org/packages/49/1c/9f6d13cc8041c05eebff1154e4e71bedd1db8e174fff999054435994187a/cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", + "url": "https://files.pythonhosted.org/packages/0f/6c/b42660b3075ff543065b2c1c5a3d9bedaadcff8ebce2ee981be2babc2934/cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", - "url": "https://files.pythonhosted.org/packages/53/c2/903014dafb7271fb148887d4355b2e90319cad6e810663be622b0c933fc9/cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl" + "hash": "ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", + "url": "https://files.pythonhosted.org/packages/58/aa/99b2c00a4f54c60d210d6d1759c720ecf28305aa32d6fb1bb1853f415be6/cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", - "url": "https://files.pythonhosted.org/packages/5c/46/de71d48abf2b6d3c808f4fbb0f4dc44a4e72786be23df0541aa2a3f6fd7e/cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl" + "hash": "4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", + "url": "https://files.pythonhosted.org/packages/5e/64/f41f42ddc9c583737c9df0093affb92c61de7d5b0d299bf644524afe31c1/cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", - "url": "https://files.pythonhosted.org/packages/5d/32/f6326c70a9f0f258a201d3b2632bca586ea24d214cec3cf36e374040e273/cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", + "url": "https://files.pythonhosted.org/packages/66/d7/397515233e6a861f921bd0365b162b38e0cc513fcf4f1bdd9cc7bc5a3384/cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", - "url": "https://files.pythonhosted.org/packages/5f/f9/c3d4f19b82bdb25a3d857fe96e7e571c981810e47e3f299cc13ac429066a/cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl" + "hash": "b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", + "url": "https://files.pythonhosted.org/packages/69/ec/9fb9dcf4f91f0e5e76de597256c43eedefd8423aa59be95c70c4c3db426a/cryptography-43.0.0.tar.gz" }, { "algorithm": "sha256", - "hash": "dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", - "url": "https://files.pythonhosted.org/packages/60/12/f064af29190cdb1d38fe07f3db6126091639e1dece7ec77c4ff037d49193/cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl" + "hash": "299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", + "url": "https://files.pythonhosted.org/packages/76/eb/ab783b47b3b9b55371b4361c7ec695144bde1a3343ff2b7a8c1d8fe617bb/cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", - "url": "https://files.pythonhosted.org/packages/89/f4/a8b982e88eb5350407ebdbf4717b55043271d878705329e107f4783555f2/cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl" + "hash": "3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", + "url": "https://files.pythonhosted.org/packages/77/9d/0b98c73cebfd41e4fb0439fe9ce08022e8d059f51caa7afc8934fc1edcd9/cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", - "url": "https://files.pythonhosted.org/packages/93/a7/1498799a2ea06148463a9a2c10ab2f6a921a74fb19e231b27dc412a748e2/cryptography-42.0.8.tar.gz" + "hash": "9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", + "url": "https://files.pythonhosted.org/packages/83/25/439a8ddd8058e7f898b7d27c36f94b66c8c8a2d60e1855d725845f4be0bc/cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", - "url": "https://files.pythonhosted.org/packages/95/26/82d704d988a193cbdc69ac3b41c687c36eaed1642cce52530ad810c35645/cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl" + "hash": "ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", + "url": "https://files.pythonhosted.org/packages/a3/62/62770f34290ebb1b6542bd3f13b3b102875b90aed4804e296f8d2a5ac6d7/cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e", - "url": "https://files.pythonhosted.org/packages/a2/68/e16751f6b859bc120f53fddbf3ebada5c34f0e9689d8af32884d8b2e4b4c/cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl" + "hash": "ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", + "url": "https://files.pythonhosted.org/packages/ae/71/e073795d0d1624847f323481f7d84855f699172a632aa37646464b0e1712/cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", - "url": "https://files.pythonhosted.org/packages/c2/de/8083fa2e68d403553a01a9323f4f8b9d7ffed09928ba25635c29fb28c1e7/cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl" + "hash": "cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", + "url": "https://files.pythonhosted.org/packages/c7/a2/1607f1295eb2c30fcf2c07d7fd0c3772d21dcdb827de2b2730b02df0af51/cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", - "url": "https://files.pythonhosted.org/packages/f9/8b/1b929ba8139430e09e140e6939c2b29c18df1f2fc2149e41bdbdcdaf5d1f/cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl" + "hash": "64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", + "url": "https://files.pythonhosted.org/packages/d3/46/dcd2eb6840b9452e7fbc52720f3dc54a85eb41e68414733379e8f98e3275/cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", - "url": "https://files.pythonhosted.org/packages/fa/5d/31d833daa800e4fab33209843095df7adb4a78ea536929145534cbc15026/cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl" + "hash": "3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", + "url": "https://files.pythonhosted.org/packages/e8/23/b0713319edff1d8633775b354f8b34a476e4dd5f4cd4b91e488baec3361a/cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", - "url": "https://files.pythonhosted.org/packages/fa/e2/b7e6e8c261536c489d9cf908769880d94bd5d9a187e166b0dc838d2e6a56/cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl" + "hash": "fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0", + "url": "https://files.pythonhosted.org/packages/f7/74/028cea86db9315ba3f991e307adabf9f0aa15067011137c38b2fb2aa16eb/cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl" } ], "project_name": "cryptography", @@ -1485,6 +1458,7 @@ "cffi>=1.12; platform_python_implementation != \"PyPy\"", "check-sdist; extra == \"pep8test\"", "click; extra == \"pep8test\"", + "cryptography-vectors==43.0.0; extra == \"test\"", "mypy; extra == \"pep8test\"", "nox; extra == \"nox\"", "pretend; extra == \"test\"", @@ -1501,7 +1475,7 @@ "sphinxcontrib-spelling>=4.0.1; extra == \"docstest\"" ], "requires_python": ">=3.7", - "version": "42.0.8" + "version": "43.0.0" }, { "artifacts": [ @@ -1715,23 +1689,23 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "53326ea2ebec768070a94bee4e1b9194c9646ea0c2bd72422785bd0f9abfad7b", - "url": "https://files.pythonhosted.org/packages/e7/00/85c22f7f73fa2e88dfbf0e1f63c565386ba40e0264b59c8a4362ae27c9fc/google_auth-2.32.0-py2.py3-none-any.whl" + "hash": "72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", + "url": "https://files.pythonhosted.org/packages/bb/fb/9af9e3f2996677bdda72734482934fe85a3abde174e5f0783ac2f817ba98/google_auth-2.34.0-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "49315be72c55a6a37d62819e3573f6b416aca00721f7e3e31a008d928bf64022", - "url": "https://files.pythonhosted.org/packages/8c/a3/cc4dc2e8a7f67012a26dec5b6b1fdf5261b12e7d54981c88de2580315726/google_auth-2.32.0.tar.gz" + "hash": "8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc", + "url": "https://files.pythonhosted.org/packages/0f/ae/634dafb151366d91eb848a25846a780dbce4326906ef005d199723fbbca0/google_auth-2.34.0.tar.gz" } ], "project_name": "google-auth", "requires_dists": [ "aiohttp<4.0.0.dev0,>=3.6.2; extra == \"aiohttp\"", "cachetools<6.0,>=2.0.0", - "cryptography==36.0.2; extra == \"enterprise-cert\"", + "cryptography; extra == \"enterprise-cert\"", "cryptography>=38.0.3; extra == \"pyopenssl\"", "pyasn1-modules>=0.2.1", - "pyopenssl==22.0.0; extra == \"enterprise-cert\"", + "pyopenssl; extra == \"enterprise-cert\"", "pyopenssl>=20.0.0; extra == \"pyopenssl\"", "pyu2f>=0.1.5; extra == \"reauth\"", "requests<3.0.0.dev0,>=2.20.0; extra == \"aiohttp\"", @@ -1739,7 +1713,7 @@ "rsa<5,>=3.1.4" ], "requires_python": ">=3.7", - "version": "2.32.0" + "version": "2.34.0" }, { "artifacts": [ @@ -2013,79 +1987,79 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "7298562a49d95570ab1c7fc4051e72824c6a80e907993a21a41ba204223e7334", - "url": "https://files.pythonhosted.org/packages/e3/8e/31c66f2e1f5b28ef7872b49e991f7442415495727f10055e547c6ea566ed/hiredis-2.3.2-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", + "url": "https://files.pythonhosted.org/packages/56/4f/5f36865f9f032caf00d603ff9cbde21506d2b1e0e0ce0b5d2ce2851411c9/hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "9c431431abf55b64347ddc8df68b3ef840269cb0aa5bc2d26ad9506eb4b1b866", - "url": "https://files.pythonhosted.org/packages/0f/47/c0ea174d1b9416f5847f9010d2879ab4493ad104c8dbdec868779cee210a/hiredis-2.3.2-cp312-cp312-macosx_10_15_x86_64.whl" + "hash": "fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", + "url": "https://files.pythonhosted.org/packages/1c/e8/1a7a5ded4fb11e91aafc5ba5518392f22883d54e79c4b47f188fb712ea46/hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl" }, { "algorithm": "sha256", - "hash": "92830c16885f29163e1c2da1f3c1edb226df1210ec7e8711aaabba3dd0d5470a", - "url": "https://files.pythonhosted.org/packages/17/33/25716dbb79df5f9221e6d1fe9db47178a449c2046acd317ba6683e00570e/hiredis-2.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", + "url": "https://files.pythonhosted.org/packages/1e/0f/f5aba1c82977f4b639e5b450c0d8685333f1200cd1972647eb3f4d972e55/hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "8f34801b251ca43ad70691fb08b606a2e55f06b9c9fb1fc18fd9402b19d70f7b", - "url": "https://files.pythonhosted.org/packages/1d/bd/ca13dc4341e57a7405246e7857054bb3a4cc9546f9030fa6001c05e62459/hiredis-2.3.2-cp312-cp312-musllinux_1_1_s390x.whl" + "hash": "0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", + "url": "https://files.pythonhosted.org/packages/3b/f5/4e055dc9b55484644afb18063f28649cdbd19be4f15bc152bd633dccd6f7/hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "49532d7939cc51f8e99efc326090c54acf5437ed88b9c904cc8015b3c4eda9c9", - "url": "https://files.pythonhosted.org/packages/4a/6c/8ab99e4fead92498dc9cfe5eb0a6b712b97ab0d93d95809d36b75a38cd37/hiredis-2.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl" + "hash": "8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", + "url": "https://files.pythonhosted.org/packages/45/02/34d9b151f9ea4655bfe00e0230f7db8fd8a52c7b7bd728efdf1c17655860/hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "5c614552c6bd1d0d907f448f75550f6b24fb56cbfce80c094908b7990cad9702", - "url": "https://files.pythonhosted.org/packages/50/69/51db3aaac7c862b3023675cf71c0f41d9abb82edceb7fe31b3872238f861/hiredis-2.3.2-cp312-cp312-macosx_10_15_universal2.whl" + "hash": "f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", + "url": "https://files.pythonhosted.org/packages/60/86/aa24c20f6d3038bf244bc60a2fe8cde61fb3c0d6a82e2bed30b08d55f96c/hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d711c107e83117129b7f8bd08e9820c43ceec6204fff072a001fd82f6d13db9f", - "url": "https://files.pythonhosted.org/packages/54/8e/379a340be1bc2e39d8060c97ac050bb0f5b0c78d69bcf7384d153dc1030e/hiredis-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", + "url": "https://files.pythonhosted.org/packages/65/7b/e06f55b9dcdf10cb6b3f08d7917d3080096cd83deaef1bd4927720fbb280/hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "5986fb5f380169270a0293bebebd95466a1c85010b4f1afc2727e4d17c452512", - "url": "https://files.pythonhosted.org/packages/5c/35/559d858578b39836d4d5a824bacc8fbd3fecdf76000454f352cf38343931/hiredis-2.3.2-cp312-cp312-musllinux_1_1_i686.whl" + "hash": "48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", + "url": "https://files.pythonhosted.org/packages/6a/30/f33f2b782096efe9fe6b24c67a4df13b5055d9c859f615a74fb4f18cce41/hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl" }, { "algorithm": "sha256", - "hash": "387f655444d912a963ab68abf64bf6e178a13c8e4aa945cb27388fd01a02e6f1", - "url": "https://files.pythonhosted.org/packages/63/2f/92ce21c16d31ba48a0ab29a7ca8d418adf755fa3396abafe1dc5150526d8/hiredis-2.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", + "url": "https://files.pythonhosted.org/packages/6e/03/a4c7a28b6320ef3e36062c1c51e9d66e889c9e09ee7d7ae38b8a2ffdb365/hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "4852f4bf88f0e2d9bdf91279892f5740ed22ae368335a37a52b92a5c88691140", - "url": "https://files.pythonhosted.org/packages/8d/68/97535298184446c7dfa6bf8c5a7bce0ca5a105a85ffd0b2e4ecaed8cd721/hiredis-2.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441", + "url": "https://files.pythonhosted.org/packages/8b/80/740fb0dfa7a42416ce8376490f41dcdb1e5deed9c3739dfe4200fad865a9/hiredis-3.0.0.tar.gz" }, { "algorithm": "sha256", - "hash": "a45857e87e9d2b005e81ddac9d815a33efd26ec67032c366629f023fe64fb415", - "url": "https://files.pythonhosted.org/packages/92/63/5bf5c439c1af0b7480b7f75f6af02eb7cdcd70b882b50597b68178c3f7ef/hiredis-2.3.2-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", + "url": "https://files.pythonhosted.org/packages/ae/09/0a3eace00115d8c82a8e7d8e58e60aacec10334f4f1512f09ffbac3252e3/hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl" }, { "algorithm": "sha256", - "hash": "e138d141ec5a6ec800b6d01ddc3e5561ce1c940215e0eb9960876bfde7186aae", - "url": "https://files.pythonhosted.org/packages/95/a0/e59d94e353bcb745149d3c0265ce972b059a03ceb583d575b49ed2124a32/hiredis-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", + "url": "https://files.pythonhosted.org/packages/cd/66/d60106b56ba0ddd9789656d204a577591ff0cd91ab94178bb96c84d0d918/hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "16b01d9ceae265d4ab9547be0cd628ecaff14b3360357a9d30c029e5ae8b7e7f", - "url": "https://files.pythonhosted.org/packages/d2/50/e4e5ebbfbd844e087d34ddb7404e9bece9ac46f9e96fd6c8534498871033/hiredis-2.3.2-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", + "url": "https://files.pythonhosted.org/packages/cf/54/68285d208918b6d83e32d872d8dcbf8d479ed2c74b863b836e48a2702a3f/hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl" }, { "algorithm": "sha256", - "hash": "733e2456b68f3f126ddaf2cd500a33b25146c3676b97ea843665717bda0c5d43", - "url": "https://files.pythonhosted.org/packages/fe/2d/a5ae61da1157644f7e52e088fa158ac6f5d09775112d14b1c9b9a5156bf1/hiredis-2.3.2.tar.gz" + "hash": "e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", + "url": "https://files.pythonhosted.org/packages/f4/16/081e90137bb896acd9dc2e1e68480cc84d652af4d959e75e52d6ce9dd602/hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" } ], "project_name": "hiredis", "requires_dists": [], - "requires_python": ">=3.7", - "version": "2.3.2" + "requires_python": ">=3.8", + "version": "3.0.0" }, { "artifacts": [ @@ -2135,19 +2109,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", - "url": "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl" + "hash": "050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "url": "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "url": "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz" + "hash": "d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", + "url": "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz" } ], "project_name": "idna", "requires_dists": [], - "requires_python": ">=3.5", - "version": "3.7" + "requires_python": ">=3.6", + "version": "3.8" }, { "artifacts": [ @@ -2572,29 +2546,19 @@ "artifacts": [ { "algorithm": "sha256", -<<<<<<< HEAD - "hash": "86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1", - "url": "https://files.pythonhosted.org/packages/96/d7/f318261e6ccbba86bdf626e07cd850981508fdaec52cfcdc4ac1030327ab/marshmallow-3.21.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662", - "url": "https://files.pythonhosted.org/packages/d6/31/0881962e77efa2d524ca80566ba1fb7cab000edaa9f4152b97a39b8d9a2d/marshmallow-3.21.3.tar.gz" -======= - "hash": "70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1", - "url": "https://files.pythonhosted.org/packages/be/24/cbb242420021a79c87768dcd22ce028f48ef40913239ad6106c8a557f52c/marshmallow-3.21.2-py3-none-any.whl" + "hash": "71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9", + "url": "https://files.pythonhosted.org/packages/3c/78/c1de55eb3311f2c200a8b91724414b8d6f5ae78891c15d9d936ea43c3dba/marshmallow-3.22.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56", - "url": "https://files.pythonhosted.org/packages/a2/16/06ad266adc423f9d7ee49dce26787b973907aa70213760c9fe1711745405/marshmallow-3.21.2.tar.gz" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "hash": "4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e", + "url": "https://files.pythonhosted.org/packages/70/40/faa10dc4500bca85f41ca9d8cefab282dd23d0fcc7a9b5fab40691e72e76/marshmallow-3.22.0.tar.gz" } ], "project_name": "marshmallow", "requires_dists": [ - "alabaster==0.7.16; extra == \"docs\"", - "autodocsumm==0.2.12; extra == \"docs\"", + "alabaster==1.0.0; extra == \"docs\"", + "autodocsumm==0.2.13; extra == \"docs\"", "marshmallow[tests]; extra == \"dev\"", "packaging>=17.0", "pre-commit~=3.5; extra == \"dev\"", @@ -2603,15 +2567,11 @@ "simplejson; extra == \"tests\"", "sphinx-issues==4.1.0; extra == \"docs\"", "sphinx-version-warning==1.1.2; extra == \"docs\"", - "sphinx==7.3.7; extra == \"docs\"", + "sphinx==8.0.2; extra == \"docs\"", "tox; extra == \"dev\"" ], "requires_python": ">=3.8", -<<<<<<< HEAD - "version": "3.21.3" -======= - "version": "3.21.2" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "version": "3.22.0" }, { "artifacts": [ @@ -2680,59 +2640,59 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "c8d6779aaaa5bfacee74df1fc23aee68f9d445a1e9a9cc3942ca70d4b1fa5ddb", - "url": "https://files.pythonhosted.org/packages/18/a6/904c7bd0e6d9d8c223ca7d4f542811078bf960201c9451ed983a18ac2ece/msgpack-1.1.0rc1-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "64a1274ac484a31afc7befbf2127ebbc35acc5905326d2ae933cc0979b25e194", + "url": "https://files.pythonhosted.org/packages/c4/f3/1469712ed4791211c48ef960bbf2b3dc8fa3cb970a423814eeda432fcc71/msgpack-1.1.0rc2-cp312-cp312-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "1567a7a089cd2dabfdf667f9a555702cfdd6bacc95538e5376e0a0d3e1cfec13", - "url": "https://files.pythonhosted.org/packages/00/04/a9dc5983cb24231eafb8cb474bc15997986ba9925d5c2a143964d243cc25/msgpack-1.1.0rc1-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "6f4368bb625b03665e667e1746eb9773021b8ef5acc05154d235794e26142b0a", + "url": "https://files.pythonhosted.org/packages/21/80/33bf52680c3b5d5351eeeb315cded93be1cca21039aaff4df149401612c8/msgpack-1.1.0rc2-cp312-cp312-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "3a4698fe8974242fccd90e99dd71f963b23bb414d3c1f2bef4c6df662ff7627f", - "url": "https://files.pythonhosted.org/packages/25/24/cac4e9966816f8c7c8e69e95fa86fa3d5bc2eaa406bab23ef28aca15a509/msgpack-1.1.0rc1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "f634b4e753bed60aed90feeaae6d20e0b9a4997287486a554edce8295ff43620", + "url": "https://files.pythonhosted.org/packages/28/aa/e1a5eb5a6688817f10c30de7e02459c3fc2cfa30edcfd918cfb878cc9066/msgpack-1.1.0rc2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "6570b5e0a006748b65f652462c872cab2d54648ff2b32e596875480558b81946", - "url": "https://files.pythonhosted.org/packages/3b/05/21b6523ea7286f238c9dc1facae615f576b676633dac23eb718c81ac33f8/msgpack-1.1.0rc1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "5f0816442d299f9450bcac0a86e0fe33e066bab8607a8721c85e8ae35dc517d7", + "url": "https://files.pythonhosted.org/packages/31/e0/8e2490a17aedce620969f18177d7cba3a457ba902c53c6f2ddbfa40b667d/msgpack-1.1.0rc2-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f5caa3b3b0243516af8e2385746991fddd29d6b7adbfe217e21d8d2fac3101ac", - "url": "https://files.pythonhosted.org/packages/60/41/39f1219e43f6ae363fd518e41bb3b400e58afc1472bc998dda0632ebbd8a/msgpack-1.1.0rc1-cp312-cp312-musllinux_1_1_i686.whl" + "hash": "0eaaaf47f5c305346825261d2aa82a81089c5536f481fcbb2d0cd40d232ebc39", + "url": "https://files.pythonhosted.org/packages/74/4c/3eac80b5f1df1ef25e153f190669b408c28fd7a878a0a321156c712b3b67/msgpack-1.1.0rc2-cp312-cp312-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "620033ee62234c83ac4ab420d0bdcd0e751f145834997135a197cc865c02eb58", - "url": "https://files.pythonhosted.org/packages/90/ee/fddd0d20802b294b416b6c576f2e26e3b54a8f1628064fe9e39602b2403b/msgpack-1.1.0rc1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "1ebf3827062ca595ecf037a811d901fd8243c508ce9017e693c2018b635a9fd5", + "url": "https://files.pythonhosted.org/packages/8e/19/50957487c004f052fba6fcf16500279ffd77b47a424273cf038d9c5ab8c0/msgpack-1.1.0rc2-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "a1d3291999cc1af4b23d394b37a6dbf5a0a16da97e50c471008eb3a4ea95ea43", - "url": "https://files.pythonhosted.org/packages/c0/e5/e1126c96f726a0baaa8f814c119cb5358f740497ee617b518497b6037d23/msgpack-1.1.0rc1.tar.gz" + "hash": "3861c5c7fb9373556abd6fcefa48ddb10cd85f7e64a826050be1a315ecb504bf", + "url": "https://files.pythonhosted.org/packages/c5/c6/b522b7afaf2ab45818d20fa5bcbdda4907d5298c4c49dc9003d5ce17a480/msgpack-1.1.0rc2-cp312-cp312-musllinux_1_2_i686.whl" }, { "algorithm": "sha256", - "hash": "57e45e59f6d45d9bdf4a5a19ae1dd3e151ab42a8dba4bc2aa5e8c4281c9d71d5", - "url": "https://files.pythonhosted.org/packages/ea/2c/e8af682b90733a62af2436b3f27a2ff828bb1ee340e53853b4730ac6f579/msgpack-1.1.0rc1-cp312-cp312-macosx_10_9_universal2.whl" + "hash": "e04b4690f5c8c2647c0ee4538b02c3730cac76f55fda736a62c63dacd11ed285", + "url": "https://files.pythonhosted.org/packages/c8/8a/f1ea0248468c1bd980b25aa751d04334c9c14e65a4a25488a7639df9ad5f/msgpack-1.1.0rc2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "23425589809b96ad7d5d00e691ae3ab65c1f0934a817b69b244fc236236f3477", - "url": "https://files.pythonhosted.org/packages/eb/9a/1396d6512d1dae98f7c6e95746c9a106f6861ba7966a70a9d8832f1faac0/msgpack-1.1.0rc1-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "07d227bc06b67ca921625cf34c2ef4858626169a583ba486f82de7e0836544ed", + "url": "https://files.pythonhosted.org/packages/cc/15/cdf2f2dedbc2da185c984334774e23616b9bab97c0a30da0f3f8b937226b/msgpack-1.1.0rc2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "aec097b46b2e2e47229a304379d9b9bfd57845c6e8c0ef078030fcd6f21e04fd", - "url": "https://files.pythonhosted.org/packages/fc/49/d2e7d114ea72546a9d0cc432b22d458726fd6ab24cf49757943c98dc4573/msgpack-1.1.0rc1-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "83d82af10ac6c9a59a6fcce74cb0acc756d3ec7b452026b474d0a56827691ff5", + "url": "https://files.pythonhosted.org/packages/e0/17/d20bb3d12b967611aaea99f74f578217cf5d45a218652b300ea0101a5e49/msgpack-1.1.0rc2.tar.gz" } ], "project_name": "msgpack", "requires_dists": [], "requires_python": ">=3.8", - "version": "1.1.0rc1" + "version": "1.1.0rc2" }, { "artifacts": [ @@ -3030,34 +2990,34 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9", - "url": "https://files.pythonhosted.org/packages/f4/d5/db585a5e8d64af6b384c7b3a63da13df2ff86933e486ba78431736c67c25/protobuf-4.25.3-py3-none-any.whl" + "hash": "bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978", + "url": "https://files.pythonhosted.org/packages/b5/95/0ba7f66934a0a798006f06fc3d74816da2b7a2bcfd9b98c53d26f684c89e/protobuf-4.25.4-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d", - "url": "https://files.pythonhosted.org/packages/15/db/7f731524fe0e56c6b2eb57d05b55d3badd80ef7d1f1ed59db191b2fdd8ab/protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl" + "hash": "eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b", + "url": "https://files.pythonhosted.org/packages/34/ca/bf85ffe3dd16f1f2aaa6c006da8118800209af3da160ae4d4f47500eabd9/protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c", - "url": "https://files.pythonhosted.org/packages/5e/d8/65adb47d921ce828ba319d6587aa8758da022de509c3862a70177a958844/protobuf-4.25.3.tar.gz" + "hash": "4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835", + "url": "https://files.pythonhosted.org/packages/68/1d/e8961af9a8e534d66672318d6b70ea8e3391a6b13e16a29b039e4a99c214/protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019", - "url": "https://files.pythonhosted.org/packages/d8/82/aefe901174b5a618daee511ddd00342193c1b545e3cd6a2cd6df9ba452b5/protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl" + "hash": "3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040", + "url": "https://files.pythonhosted.org/packages/ca/6c/cc7ab2fb3a4a7f07f211d8a7bbb76bba633eb09b148296dbd4281e217f95/protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c", - "url": "https://files.pythonhosted.org/packages/f3/bf/26deba06a4c910a85f78245cac7698f67cedd7efe00d04f6b3e1b3506a59/protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl" + "hash": "0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d", + "url": "https://files.pythonhosted.org/packages/e8/ab/cb61a4b87b2e7e6c312dce33602bd5884797fd054e0e53205f1c27cf0f66/protobuf-4.25.4.tar.gz" } ], "project_name": "protobuf", "requires_dists": [], "requires_python": ">=3.8", - "version": "4.25.3" + "version": "4.25.4" }, { "artifacts": [ @@ -3411,13 +3371,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", - "url": "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl" + "hash": "3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "url": "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", - "url": "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz" + "hash": "7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", + "url": "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz" } ], "project_name": "pyjwt", @@ -3431,36 +3391,25 @@ "pytest<7.0.0,>=6.0.0; extra == \"tests\"", "sphinx-rtd-theme; extra == \"dev\"", "sphinx-rtd-theme; extra == \"docs\"", - "sphinx<5.0.0,>=4.5.0; extra == \"dev\"", - "sphinx<5.0.0,>=4.5.0; extra == \"docs\"", - "typing-extensions; python_version <= \"3.7\"", + "sphinx; extra == \"dev\"", + "sphinx; extra == \"docs\"", "zope.interface; extra == \"dev\"", "zope.interface; extra == \"docs\"" ], - "requires_python": ">=3.7", - "version": "2.8.0" + "requires_python": ">=3.8", + "version": "2.9.0" }, { "artifacts": [ { "algorithm": "sha256", -<<<<<<< HEAD - "hash": "c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", - "url": "https://files.pythonhosted.org/packages/4e/e7/81ebdd666d3bff6670d27349b5053605d83d55548e6bd5711f3b0ae7dd23/pytest-8.2.2-py3-none-any.whl" + "hash": "4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", + "url": "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977", - "url": "https://files.pythonhosted.org/packages/a6/58/e993ca5357553c966b9e73cb3475d9c935fe9488746e13ebdf9b80fae508/pytest-8.2.2.tar.gz" -======= - "hash": "1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", - "url": "https://files.pythonhosted.org/packages/c4/43/6b1debd95ecdf001bc46789a933f658da3f9738c65f32db3f4e8f2a4ca97/pytest-8.2.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f", - "url": "https://files.pythonhosted.org/packages/09/9d/78b3785134306efe9329f40815af45b9215068d6ae4747ec0bc91ff1f4aa/pytest-8.2.0.tar.gz" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "hash": "c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", + "url": "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz" } ], "project_name": "pytest", @@ -3473,7 +3422,7 @@ "iniconfig", "mock; extra == \"dev\"", "packaging", - "pluggy<2.0,>=1.5", + "pluggy<2,>=1.5", "pygments>=2.7.2; extra == \"dev\"", "requests; extra == \"dev\"", "setuptools; extra == \"dev\"", @@ -3481,11 +3430,7 @@ "xmlschema; extra == \"dev\"" ], "requires_python": ">=3.8", -<<<<<<< HEAD - "version": "8.2.2" -======= - "version": "8.2.0" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "version": "8.3.2" }, { "artifacts": [ @@ -3565,39 +3510,49 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "url": "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "url": "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "url": "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "url": "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "url": "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "url": "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "url": "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "url": "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "url": "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "url": "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "url": "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "url": "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "url": "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz" + "hash": "0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "url": "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl" } ], "project_name": "pyyaml", "requires_dists": [], - "requires_python": ">=3.6", - "version": "6.0.1" + "requires_python": ">=3.8", + "version": "6.0.2" }, { "artifacts": [ @@ -3663,42 +3618,37 @@ "artifacts": [ { "algorithm": "sha256", -<<<<<<< HEAD - "hash": "d163680656b34f263fb5074023db44b999c68ff31ab394445ebfd1a2a41fe9a2", - "url": "https://files.pythonhosted.org/packages/6b/cd/feba6c20ae4b00d424e6fd802edd4e1557e500501b376c8111a60ba1b83f/readchar-4.1.0-py3-none-any.whl" -======= - "hash": "04c5983aed2c700eb0f3100ac09e51ce5c65ba39d8a627ec138d385261a6a850", - "url": "https://files.pythonhosted.org/packages/78/76/118c5e454a68014e4580cdc2aa0cb87a12c51f40bc57b420ae3daec102ee/raftify-0.1.65-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "583592ab9650bc3041a8532d0e803e6588a19a8032ec9f590469cd01990c3bd6", + "url": "https://files.pythonhosted.org/packages/72/00/a8d51149e180df3c0b816137aee4da4a7d07b095fb880ea868364b6b8e5c/raftify-0.1.67-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "71abf9d3e3e829850cba5b013a42c6e9ef2d556585bf224de5aff75a182a93c2", - "url": "https://files.pythonhosted.org/packages/df/e0/49cb08a23f49a232d94fd3a32f534654750930e98cf85d074a9b4be3582b/raftify-0.1.65.tar.gz" + "hash": "6be8df3f3b53b1189b19e597781dceb5285a30a8d54d3441fef8dcc7a1fd68ea", + "url": "https://files.pythonhosted.org/packages/9a/f5/c8e3c5a050229fbf33787b62bc6736e6f7b8f503d516a7588daf7375483c/raftify-0.1.67.tar.gz" } ], "project_name": "raftify", "requires_dists": [], "requires_python": ">=3.10", - "version": "0.1.65" + "version": "0.1.67" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "b4b31dd35de4897be738f27e8f9f62426b5fedb54b648364987e30ae534b71bc", - "url": "https://files.pythonhosted.org/packages/86/db/aca9e5e6a53a499d61cbd78b3594d700f1e48a50ab6970a92a4d1251f8db/readchar-4.0.6-py3-none-any.whl" ->>>>>>> b9b8a242 (feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer) + "hash": "2a587a27c981e6d25a518730ad4c88c429c315439baa6fda55d7a8b3ac4cb62a", + "url": "https://files.pythonhosted.org/packages/7b/6f/ca076ad4d18b3d33c31c304fb7e68dd9ce2bfdb49fb8874611ad7c55e969/readchar-4.2.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "6f44d1b5f0fd93bd93236eac7da39609f15df647ab9cea39f5bc7478b3344b99", - "url": "https://files.pythonhosted.org/packages/23/85/a83385c8765af35c3fdd9cf67a387107b99bc545b8559e1f097c9d777dde/readchar-4.1.0.tar.gz" + "hash": "44807cbbe377b72079fea6cba8aa91c809982d7d727b2f0dbb2d1a8084914faa", + "url": "https://files.pythonhosted.org/packages/18/31/2934981710c63afa9c58947d2e676093ce4bb6c7ce60aac2fcc4be7d98d0/readchar-4.2.0.tar.gz" } ], "project_name": "readchar", "requires_dists": [], "requires_python": ">=3.8", - "version": "4.1.0" + "version": "4.2.0" }, { "artifacts": [ @@ -3777,13 +3727,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "url": "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl" + "hash": "2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc", + "url": "https://files.pythonhosted.org/packages/c7/d9/c2a126eeae791e90ea099d05cb0515feea3688474b978343f3cdcfe04523/rich-13.8.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", - "url": "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz" + "hash": "a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4", + "url": "https://files.pythonhosted.org/packages/cf/60/5959113cae0ce512cf246a6871c623117330105a0d5f59b4e26138f2c9cc/rich-13.8.0.tar.gz" } ], "project_name": "rich", @@ -3794,7 +3744,7 @@ "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"" ], "requires_python": ">=3.7.0", - "version": "13.7.1" + "version": "13.8.0" }, { "artifacts": [ @@ -4003,23 +3953,23 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "de9acf369aaadb71a725b7e83a5ef40ca3de1cf4cdc93fa847df6b12d3cd924b", - "url": "https://files.pythonhosted.org/packages/10/c1/1613a8dcd05e6dacc9505554ce6c217a1cfda0da9c7592e258856945c6b6/SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "d13d4dfbc6e52363886b47cf02cf68c5d2a37c468626694dc210d7e97d4ad330", + "url": "https://files.pythonhosted.org/packages/9c/85/27d94ae4b18d97d151b201aa68176209bcd03f6932944b0354e6cd47fb6d/SQLAlchemy-1.4.53-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296", - "url": "https://files.pythonhosted.org/packages/8a/a4/b5991829c34af0505e0f2b1ccf9588d1ba90f2d984ee208c90c985f1265a/SQLAlchemy-1.4.52.tar.gz" + "hash": "68a614765197b3d13a730d631a78c3bb9b3b72ba58ed7ab295d58d517464e315", + "url": "https://files.pythonhosted.org/packages/3a/c0/2f4e45ce194636098ca2a810ddd1a7bdfee6513de8fa0796b45ca5ef5461/SQLAlchemy-1.4.53-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554", - "url": "https://files.pythonhosted.org/packages/ce/e6/9da1e081321a514c0147a2e0b293f27ca93f0f299cbd5ba746a9422a9f07/SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "2774c24c405136c3ef472e2352bdca7330659d481fbf2283f996c0ef9eb90f22", + "url": "https://files.pythonhosted.org/packages/75/f8/6e04c212f8b495c9ec324a5079a57e0d97b49324a14c393388cf8fa100bb/SQLAlchemy-1.4.53-cp312-cp312-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c", - "url": "https://files.pythonhosted.org/packages/fc/30/7e04f16d0508d4e57edd5c8def5810bb31bc73203beacd8bf83ed18ff0f1/SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl" + "hash": "5e6ab710c4c064755fd92d1a417bef360228a19bdf0eee32b03aa0f5f8e9fe0d", + "url": "https://files.pythonhosted.org/packages/e8/f1/40d71bba877e1c92d5995de3605d6256be1cc148d2ca0475812f156d7a58/SQLAlchemy-1.4.53.tar.gz" } ], "project_name": "sqlalchemy", @@ -4027,6 +3977,7 @@ "aiomysql>=0.2.0; python_version >= \"3\" and extra == \"aiomysql\"", "aiosqlite; python_version >= \"3\" and extra == \"aiosqlite\"", "asyncmy!=0.2.4,>=0.2.3; python_version >= \"3\" and extra == \"asyncmy\"", + "asyncpg; python_version >= \"3\" and extra == \"postgresql-asyncpg\"", "asyncpg; python_version >= \"3\" and extra == \"postgresql_asyncpg\"", "cx-oracle<8,>=7; python_version < \"3\" and extra == \"oracle\"", "cx-oracle>=7; python_version >= \"3\" and extra == \"oracle\"", @@ -4035,28 +3986,34 @@ "greenlet!=0.4.17; python_version >= \"3\" and extra == \"aiosqlite\"", "greenlet!=0.4.17; python_version >= \"3\" and extra == \"asyncio\"", "greenlet!=0.4.17; python_version >= \"3\" and extra == \"asyncmy\"", + "greenlet!=0.4.17; python_version >= \"3\" and extra == \"postgresql-asyncpg\"", "greenlet!=0.4.17; python_version >= \"3\" and extra == \"postgresql_asyncpg\"", "importlib-metadata; python_version < \"3.8\"", + "mariadb!=1.1.2,>=1.0.1; python_version >= \"3\" and extra == \"mariadb-connector\"", "mariadb!=1.1.2,>=1.0.1; python_version >= \"3\" and extra == \"mariadb_connector\"", "mypy>=0.910; python_version >= \"3\" and extra == \"mypy\"", "mysql-connector-python; extra == \"mysql-connector\"", + "mysql-connector-python; extra == \"mysql-connector\"", "mysqlclient<2,>=1.4.0; python_version < \"3\" and extra == \"mysql\"", "mysqlclient>=1.4.0; python_version >= \"3\" and extra == \"mysql\"", - "pg8000!=1.29.0,>=1.16.6; extra == \"postgresql-pg8000\"", + "pg8000!=1.29.0,>=1.16.6; python_version >= \"3\" and extra == \"postgresql-pg8000\"", + "pg8000!=1.29.0,>=1.16.6; python_version >= \"3\" and extra == \"postgresql_pg8000\"", "psycopg2-binary; extra == \"postgresql-psycopg2binary\"", "psycopg2>=2.7; extra == \"postgresql\"", "psycopg2cffi; extra == \"postgresql-psycopg2cffi\"", "pymssql; extra == \"mssql-pymssql\"", + "pymssql; extra == \"mssql-pymssql\"", "pymysql; python_version >= \"3\" and extra == \"pymysql\"", "pymysql<1; python_version < \"3\" and extra == \"pymysql\"", "pyodbc; extra == \"mssql\"", "pyodbc; extra == \"mssql-pyodbc\"", + "pyodbc; extra == \"mssql-pyodbc\"", "sqlalchemy2-stubs; extra == \"mypy\"", "sqlcipher3-binary; python_version >= \"3\" and extra == \"sqlcipher\"", "typing-extensions!=3.10.0.1; extra == \"aiosqlite\"" ], "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", - "version": "1.4.52" + "version": "1.4.53" }, { "artifacts": [ @@ -4095,13 +4052,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", - "url": "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl" + "hash": "93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", + "url": "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", - "url": "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz" + "hash": "807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", + "url": "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz" } ], "project_name": "tenacity", @@ -4113,7 +4070,7 @@ "typeguard; extra == \"test\"" ], "requires_python": ">=3.8", - "version": "8.5.0" + "version": "9.0.0" }, { "artifacts": [ @@ -4250,13 +4207,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644", - "url": "https://files.pythonhosted.org/packages/18/eb/fdb7eb9e48b7b02554e1664afd3bd3f117f6b6d6c5881438a0b055554f9b/tqdm-4.66.4-py3-none-any.whl" + "hash": "90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", + "url": "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb", - "url": "https://files.pythonhosted.org/packages/5a/c0/b7599d6e13fe0844b0cda01b9aaef9a0e87dbb10b06e4ee255d3fa1c79a2/tqdm-4.66.4.tar.gz" + "hash": "e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", + "url": "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz" } ], "project_name": "tqdm", @@ -4271,7 +4228,7 @@ "slack-sdk; extra == \"slack\"" ], "requires_python": ">=3.7", - "version": "4.66.4" + "version": "4.66.5" }, { "artifacts": [ @@ -4388,19 +4345,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "98c069dc7fc087b1b061703369c80751b0a0fc561f6fb072b554e5eee23773a0", - "url": "https://files.pythonhosted.org/packages/9a/2b/93146a80105a1ab180f15a5457d45706578ff0e20ba186d0d2ba7a75e3c3/types_cachetools-5.3.0.7-py3-none-any.whl" + "hash": "efb2ed8bf27a4b9d3ed70d33849f536362603a90b8090a328acf0cd42fda82e2", + "url": "https://files.pythonhosted.org/packages/27/4d/fd7cc050e2d236d5570c4d92531c0396573a1e14b31735870e849351c717/types_cachetools-5.5.0.20240820-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "27c982cdb9cf3fead8b0089ee6b895715ecc99dac90ec29e2cab56eb1aaf4199", - "url": "https://files.pythonhosted.org/packages/ef/bb/51a12d05e492d1648cd1cfc5b212f81fc8a8a5ea1610423574e7deea15ea/types-cachetools-5.3.0.7.tar.gz" + "hash": "b888ab5c1a48116f7799cd5004b18474cd82b5463acb5ffb2db2fc9c7b053bc0", + "url": "https://files.pythonhosted.org/packages/c2/7e/ad6ba4a56b2a994e0f0a04a61a50466b60ee88a13d10a18c83ac14a66c61/types-cachetools-5.5.0.20240820.tar.gz" } ], "project_name": "types-cachetools", "requires_dists": [], - "requires_python": ">=3.7", - "version": "5.3.0.7" + "requires_python": ">=3.8", + "version": "5.5.0.20240820" }, { "artifacts": [ @@ -4464,13 +4421,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "f51a156835555dd2a1f025621e8c4fbe7493470331afeef96884d1d29bf3a473", - "url": "https://files.pythonhosted.org/packages/b6/7b/555c69a26d733c5254657d4a9fc4b3ec3ed13c73336f06c68ebd352258d0/types_pyOpenSSL-24.1.0.20240425-py3-none-any.whl" + "hash": "6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", + "url": "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0a7e82626c1983dc8dc59292bf20654a51c3c3881bcbb9b337c1da6e32f0204e", - "url": "https://files.pythonhosted.org/packages/ba/99/0ef33ca243dc4a9306ed7c976dd2d8563e3cf89371456d575f5b13f7dc68/types-pyOpenSSL-24.1.0.20240425.tar.gz" + "hash": "47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", + "url": "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz" } ], "project_name": "types-pyopenssl", @@ -4479,55 +4436,55 @@ "types-cffi" ], "requires_python": ">=3.8", - "version": "24.1.0.20240425" + "version": "24.1.0.20240722" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b", - "url": "https://files.pythonhosted.org/packages/c7/1b/af4f4c4f3f7339a4b7eb3c0ab13416db98f8ac09de3399129ee5fdfa282b/types_python_dateutil-2.9.0.20240316-py3-none-any.whl" + "hash": "f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57", + "url": "https://files.pythonhosted.org/packages/45/ba/2a4750156272f180f8209f87656ae92e0aeb14f9864976aa90cbd9f21eda/types_python_dateutil-2.9.0.20240821-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202", - "url": "https://files.pythonhosted.org/packages/61/c5/c3a4d72ffa8efc2e78f7897b1c69ec760553246b67d3ce8c4431fac5d4e3/types-python-dateutil-2.9.0.20240316.tar.gz" + "hash": "9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98", + "url": "https://files.pythonhosted.org/packages/23/11/aae06ddb6a90cf8ba078be6dbe47f904d2efdf451f9859248b436c945ca4/types-python-dateutil-2.9.0.20240821.tar.gz" } ], "project_name": "types-python-dateutil", "requires_dists": [], "requires_python": ">=3.8", - "version": "2.9.0.20240316" + "version": "2.9.0.20240821" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6", - "url": "https://files.pythonhosted.org/packages/b3/9a/2b75087549910ebd2be9894bfd89450668b2455094a8f2ba2b67072f15a5/types_PyYAML-6.0.12.20240311-py3-none-any.whl" + "hash": "deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35", + "url": "https://files.pythonhosted.org/packages/f3/ad/ffbad24e2bc8f20bf047ec22af0c0a92f6ce2071eb21c9103df600cda6de/types_PyYAML-6.0.12.20240808-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", - "url": "https://files.pythonhosted.org/packages/0a/3c/6f4c97d9eb2b58f57fc595c105ae0a53a851747cddfb7df30f3d7192c837/types-PyYAML-6.0.12.20240311.tar.gz" + "hash": "b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af", + "url": "https://files.pythonhosted.org/packages/dd/08/6f5737f645571b7a0b1ebd2fe8b5cf1ee4ec3e707866ca96042a86fc1d10/types-PyYAML-6.0.12.20240808.tar.gz" } ], "project_name": "types-pyyaml", "requires_dists": [], "requires_python": ">=3.8", - "version": "6.0.12.20240311" + "version": "6.0.12.20240808" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "ac5bc19e8f5997b9e76ad5d9cf15d0392d9f28cf5fc7746ea4a64b989c45c6a8", - "url": "https://files.pythonhosted.org/packages/82/79/9de458746a7907f7bd2e96ff7ee1538aa9066683d1276d4494f2b8c7678b/types_redis-4.6.0.20240425-py3-none-any.whl" + "hash": "86db9af6f0033154e12bc22c77236cef0907b995fda8c9f0f0eacd59943ed2fc", + "url": "https://files.pythonhosted.org/packages/fd/5d/f636b3bb65d52705abc9eb5832864dbd99d26ad1b1c2b5c2ff24af12249d/types_redis-4.6.0.20240819-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9402a10ee931d241fdfcc04592ebf7a661d7bb92a8dea631279f0d8acbcf3a22", - "url": "https://files.pythonhosted.org/packages/15/a9/5ba198edeccefe7c3e6e620f4737f56814309201c8dfad184590dcc3cb0a/types-redis-4.6.0.20240425.tar.gz" + "hash": "08f51f550ad41d0152bd98d77ac9d6d8f761369121710a213642f6036b9a7183", + "url": "https://files.pythonhosted.org/packages/06/2b/a09204d0901d9d319b38f26434c5544f400b2a551df9ecad9ad0437987a0/types-redis-4.6.0.20240819.tar.gz" } ], "project_name": "types-redis", @@ -4536,25 +4493,25 @@ "types-pyOpenSSL" ], "requires_python": ">=3.8", - "version": "4.6.0.20240425" + "version": "4.6.0.20240819" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "bd0db2a4b9f2c49ac5564be4e0fb3125c4c46b1f73eafdcbceffa5b005cceca4", - "url": "https://files.pythonhosted.org/packages/c3/be/60f6258da5989be4bfe1fdb1c10d4b5a722f4ca2656b20ffe1276a9d33e2/types_setuptools-70.3.0.20240710-py3-none-any.whl" + "hash": "4d9d18ea9214828d695a384633130009f5dee2681a157ee873d3680b62931590", + "url": "https://files.pythonhosted.org/packages/00/59/0c189e4fe19f976c7f74e3dcb05620629f2da2a1c7935043611dea19b4ef/types_setuptools-74.0.0.20240831-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "842cbf399812d2b65042c9d6ff35113bbf282dee38794779aa1f94e597bafc35", - "url": "https://files.pythonhosted.org/packages/78/89/081fb601a795995d0032d024bc71bf0a0c835a566f06c18c88a220f950e5/types-setuptools-70.3.0.20240710.tar.gz" + "hash": "8b4a544cc91d42a019dc1e41fd397608b4bc7e20c7d7d5bc326589ffd9e8f8a1", + "url": "https://files.pythonhosted.org/packages/e9/a9/f4ae8910009a891baed9cde41b1bb6a98e5f6199cd663f36c8d2f8ca3050/types-setuptools-74.0.0.20240831.tar.gz" } ], "project_name": "types-setuptools", "requires_dists": [], "requires_python": ">=3.8", - "version": "70.3.0.20240710" + "version": "74.0.0.20240831" }, { "artifacts": [ @@ -4800,88 +4757,87 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "url": "https://files.pythonhosted.org/packages/4d/05/4d79198ae568a92159de0f89e710a8d19e3fa267b719a236582eee921f4a/yarl-1.9.4-py3-none-any.whl" + "hash": "49935cc51d272264358962d050d726c3e5603a616f53e52ea88e9df1728aa2ee", + "url": "https://files.pythonhosted.org/packages/48/04/8cc40203453e4bce05cd3e9a5bea930ac0086aa4848a9c41aa1da13ae1a0/yarl-1.9.7-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "url": "https://files.pythonhosted.org/packages/04/e0/0029563a8434472697aebb269fdd2ffc8a19e3840add1d5fa169ec7c56e3/yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl" + "hash": "3a7748cd66fef49c877e59503e0cc76179caf1158d1080228e67e1db14554f08", + "url": "https://files.pythonhosted.org/packages/0e/7b/2fe90636cf0f745210bcb79347369a3882e829e1070ab7d8b3949684d209/yarl-1.9.7-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "url": "https://files.pythonhosted.org/packages/06/91/9696601a8ba674c8f0c15035cc9e94ca31f541330364adcfd5a399f598bf/yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "1d5594512541e63188fea640b7f066c218d2176203d6e6f82abf702ae3dca3b2", + "url": "https://files.pythonhosted.org/packages/17/7d/74a41e5d49329be134602a7e840adf3a499c7562afb982282d079067d5e4/yarl-1.9.7-cp312-cp312-musllinux_1_2_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "url": "https://files.pythonhosted.org/packages/28/1c/bdb3411467b805737dd2720b85fd082e49f59bf0cc12dc1dfcc80ab3d274/yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "867b13c1b361f9ba5d2f84dc5408082f5d744c83f66de45edc2b96793a9c5e48", + "url": "https://files.pythonhosted.org/packages/24/5d/1b982866e45906f236cb9a93ec9a07a5b61854b34f0f6fa368056ee9cda3/yarl-1.9.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "url": "https://files.pythonhosted.org/packages/2e/5e/1c78eb05ae0efae08498fd7ab939435a29f12c7f161732e7fe327e5b8ca1/yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl" + "hash": "cf37dd0008e5ac5c3880198976063c491b6a15b288d150d12833248cf2003acb", + "url": "https://files.pythonhosted.org/packages/28/60/3e985358440d6467c2ea81673000aef762c448462ab88e98e6676845f24f/yarl-1.9.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "url": "https://files.pythonhosted.org/packages/41/e9/53bc89f039df2824a524a2aa03ee0bfb8f0585b08949e7521f5eab607085/yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "daa69a3a2204355af39f4cfe7f3870d87c53d77a597b5100b97e3faa9460428b", + "url": "https://files.pythonhosted.org/packages/44/74/877076885263c214abbed93462ef2e4e95579c047d188530c849ea207846/yarl-1.9.7-cp312-cp312-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "url": "https://files.pythonhosted.org/packages/54/99/ed3c92c38f421ba6e36caf6aa91c34118771d252dce800118fa2f44d7962/yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "d06d6a8f98dd87646d98f0c468be14b201e47ec6092ad569adf835810ad0dffb", + "url": "https://files.pythonhosted.org/packages/71/ff/bce0bda27957d4f8cdb8e56b807f185683e8b6a3717637fb8d1faa39269d/yarl-1.9.7-cp312-cp312-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "url": "https://files.pythonhosted.org/packages/79/cd/a78c3b0304a4a970b5ae3993f4f5f649443bc8bfa5622f244aed44c810ed/yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "48ce93947554c2c85fe97fc4866646ec90840bc1162e4db349b37d692a811755", + "url": "https://files.pythonhosted.org/packages/8c/ec/eaab7e272ddf1eab39b793e5cd3af304ac28d9342f6a3f2e356276bcc4fe/yarl-1.9.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "url": "https://files.pythonhosted.org/packages/7b/cd/a921122610dedfed94e494af18e85aae23e93274c00ca464cfc591c8f4fb/yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl" + "hash": "fcd3d94b848cba132f39a5b40d80b0847d001a91a6f35a2204505cdd46afe1b2", + "url": "https://files.pythonhosted.org/packages/98/c3/ed093752106c61e3b2a108f798649cb24119484802bb5ca521a36cf559bd/yarl-1.9.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "url": "https://files.pythonhosted.org/packages/7c/a0/887c93020c788f249c24eaab288c46e5fed4d2846080eaf28ed3afc36e8d/yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "9c2743e43183e4afbb07d5605693299b8756baff0b086c25236c761feb0e3c56", + "url": "https://files.pythonhosted.org/packages/c1/f9/8a9083b6b73944c0bb5c99cfc0edf3bec14456b621f76b338fc945afc69f/yarl-1.9.7-cp312-cp312-musllinux_1_2_s390x.whl" }, { "algorithm": "sha256", - "hash": "3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "url": "https://files.pythonhosted.org/packages/85/8a/c364d6e2eeb4e128a5ee9a346fc3a09aa76739c0c4e2a7305989b54f174b/yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "f28e602edeeec01fc96daf7728e8052bc2e12a672e2a138561a1ebaf30fd9df7", + "url": "https://files.pythonhosted.org/packages/ce/50/dcf6d0ea0da893b23f73ea5b21fa1f96fd45e9cb4404cc6b665368b4ab19/yarl-1.9.7.tar.gz" }, { "algorithm": "sha256", - "hash": "ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "url": "https://files.pythonhosted.org/packages/da/3e/bf25177b3618889bf067aacf01ef54e910cd569d14e2f84f5e7bec23bb82/yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "0d8cf3d0b67996edc11957aece3fbce4c224d0451c7c3d6154ec3a35d0e55f6b", + "url": "https://files.pythonhosted.org/packages/dd/4e/b3d7679b158a981e6fa36c1d4388a7c3f4adb1b5c33ec22708ec550ddf91/yarl-1.9.7-cp312-cp312-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "url": "https://files.pythonhosted.org/packages/de/1b/7e6b1ad42ccc0ed059066a7ae2b6fd4bce67795d109a99ccce52e9824e96/yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl" + "hash": "4a6fa3aeca8efabb0fbbb3b15e0956b0cb77f7d9db67c107503c30af07cd9e00", + "url": "https://files.pythonhosted.org/packages/e2/ee/fae90e40bb4c2af6fd8a1a50d052140101a6634f1d2b32596a6cf53f4244/yarl-1.9.7-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "url": "https://files.pythonhosted.org/packages/e0/ad/bedcdccbcbf91363fd425a948994f3340924145c2bc8ccb296f4a1e52c28/yarl-1.9.4.tar.gz" + "hash": "87aa5308482f248f8c3bd9311cd6c7dfd98ea1a8e57e35fb11e4adcac3066003", + "url": "https://files.pythonhosted.org/packages/e3/ae/f0730026d7011f5403a8f49fec4e666358db43a0339dc8259b19a7c2e6f1/yarl-1.9.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "url": "https://files.pythonhosted.org/packages/ea/45/65801be625ef939acc8b714cf86d4a198c0646e80dc8970359d080c47204/yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "91567ff4fce73d2e7ac67ed5983ad26ba2343bc28cb22e1e1184a9677df98d7c", + "url": "https://files.pythonhosted.org/packages/ff/b5/95702c9719808331d2401e13660af86b323139f0293feb3a44698a194439/yarl-1.9.7-cp312-cp312-musllinux_1_2_i686.whl" } ], "project_name": "yarl", "requires_dists": [ "idna>=2.0", - "multidict>=4.0", - "typing-extensions>=3.7.4; python_version < \"3.8\"" + "multidict>=4.0" ], - "requires_python": ">=3.7", - "version": "1.9.4" + "requires_python": ">=3.8", + "version": "1.9.7" }, { "artifacts": [ @@ -4980,7 +4936,7 @@ "python-dotenv~=0.20.0", "python-json-logger>=2.0.1", "pyzmq~=25.1.2", - "raftify==0.1.65", + "raftify==0.1.67", "redis[hiredis]==4.5.5", "rich~=13.6", "setproctitle~=1.3.2", diff --git a/requirements.txt b/requirements.txt index c67a34c4326..f3770e3e371 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ PyYAML~=6.0 pydantic~=2.6.4 packaging>=21.3 hiredis>=2.2.3 -raftify==0.1.65 +raftify==0.1.67 redis[hiredis]==4.5.5 rich~=13.6 SQLAlchemy[postgresql_asyncpg]~=1.4.40 diff --git a/src/ai/backend/manager/api/context.py b/src/ai/backend/manager/api/context.py index fb5aeaa6a97..24e2b86b862 100644 --- a/src/ai/backend/manager/api/context.py +++ b/src/ai/backend/manager/api/context.py @@ -6,6 +6,8 @@ import attrs from raftify import Raft +from ai.backend.logging.types import CIStrEnum + if TYPE_CHECKING: from ai.backend.common.bgtask import BackgroundTaskManager from ai.backend.common.events import EventDispatcher, EventProducer @@ -28,9 +30,9 @@ class BaseContext: pass -class GlobalTimerKind(enum.StrEnum): - RAFT = "raft" - DISTRIBUTED_LOCK = "distributed_lock" +class GlobalTimerKind(CIStrEnum): + RAFT = enum.auto() + DISTRIBUTED_LOCK = enum.auto() class GlobalTimerContext: diff --git a/src/ai/backend/manager/api/logs.py b/src/ai/backend/manager/api/logs.py index 99e1307c8b0..39bf4bfd25d 100644 --- a/src/ai/backend/manager/api/logs.py +++ b/src/ai/backend/manager/api/logs.py @@ -20,7 +20,6 @@ RaftGlobalTimer, ) from ai.backend.common.events import AbstractEvent, EmptyEventArgs, EventHandler -from ai.backend.common.logging import BraceStyleAdapter from ai.backend.common.types import AgentId from ai.backend.logging import BraceStyleAdapter, LogLevel from ai.backend.manager.api.context import GlobalTimerKind diff --git a/src/ai/backend/manager/idle.py b/src/ai/backend/manager/idle.py index 7708ee5875c..dcf29010517 100644 --- a/src/ai/backend/manager/idle.py +++ b/src/ai/backend/manager/idle.py @@ -254,8 +254,8 @@ async def start(self) -> None: self.check_interval, task_name="idle_checker", ) - case _: - assert False, f"Unknown global timer backend: {self.global_timer_ctx.timer_kind}" + case _ as unknown: + assert False, f"Unknown global timer backend: {unknown}" self._evh_idle_check = self._event_dispatcher.consume( DoIdleCheckEvent, diff --git a/src/ai/backend/manager/scheduler/dispatcher.py b/src/ai/backend/manager/scheduler/dispatcher.py index 8f2e25260b8..dfb932c197b 100644 --- a/src/ai/backend/manager/scheduler/dispatcher.py +++ b/src/ai/backend/manager/scheduler/dispatcher.py @@ -80,7 +80,6 @@ InstanceNotAvailable, SessionNotFound, ) -from ..defs import SERVICE_MAX_RETRIES, LockID from ..exceptions import convert_to_status_data from ..models import ( AgentStatus, @@ -714,10 +713,8 @@ async def _update_session_status_data() -> None: agent_selection_resource_priority, check_results, ) - case _: - log.exception( - f"should not reach here; unknown cluster_mode: {schedulable_sess.cluster_mode}" - ) + case _ as unknown: + log.exception(f"should not reach here; unknown cluster_mode: {unknown}") continue except InstanceNotAvailable as e: # Proceed to the next pending session and come back later. diff --git a/src/ai/backend/manager/types.py b/src/ai/backend/manager/types.py index 6fa5953a7bb..c335b78a3b0 100644 --- a/src/ai/backend/manager/types.py +++ b/src/ai/backend/manager/types.py @@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession as SASession from ai.backend.common.types import MountPermission, MountTypes +from ai.backend.logging.types import CIStrEnum if TYPE_CHECKING: from ai.backend.common.lock import AbstractDistributedLock @@ -58,16 +59,16 @@ class MountOptionModel(BaseModel): ] -class RaftNodeInitialRole(str, enum.Enum): - LEADER = "leader" - VOTER = "voter" - LEARNER = "learner" +class RaftNodeInitialRole(CIStrEnum): + LEADER = enum.auto() + VOTER = enum.auto() + LEARNER = enum.auto() -class RaftLogLovel(enum.StrEnum): - TRACE = "trace" - DEBUG = "debug" - INFO = "info" - WARN = "warn" - ERROR = "error" - FATAL = "fatal" +class RaftLogLovel(CIStrEnum): + TRACE = enum.auto() + DEBUG = enum.auto() + INFO = enum.auto() + WARN = enum.auto() + ERROR = enum.auto() + FATAL = enum.auto()