diff --git a/changes/1506.feature.md b/changes/1506.feature.md new file mode 100644 index 00000000000..e5056d80382 --- /dev/null +++ b/changes/1506.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/client_example.py b/client_example.py new file mode 100644 index 00000000000..adb37c60917 --- /dev/null +++ b/client_example.py @@ -0,0 +1,42 @@ +import asyncio +import pickle + +from rraft import ConfChange, ConfChangeType + +from raftify.log_entry.set_command import SetCommand +from raftify.raft_client import RaftClient +from raftify.utils import SocketAddr + + +async def main() -> None: + """ + A simple set of commands to test and show usage of RaftClient. + Please bootstrap the Raft cluster before running this script. + """ + + print("---Message propose---") + await RaftClient("192.168.0.37:60151").propose(SetCommand("1", "B").encode()) + + # print("---Message propose rerouting---") + # await RaftClient("127.0.0.1:60062").propose(SetCommand("2", "A").encode()) + + # print("---Debug node result---", await RaftClient("127.0.0.1:60061").debug_node()) + + # print( + # "---Debug peers---", + # pickle.loads((await RaftClient("127.0.0.1:60061").get_peers()).peers), + # ) + + # print("---Make Confchange manually---") + # addr = SocketAddr.from_str("127.0.0.1:60062") + + # conf_change = ConfChange.default() + # conf_change.set_node_id(2) + # conf_change.set_context(pickle.dumps([addr])) + # conf_change.set_change_type(ConfChangeType.RemoveNode) + + # await RaftClient(addr).change_config(conf_change) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/configs/manager/halfstack.toml b/configs/manager/halfstack.toml index 657f39da28e..a0e7be5bdb9 100644 --- a/configs/manager/halfstack.toml +++ b/configs/manager/halfstack.toml @@ -14,7 +14,7 @@ password = "develove" [manager] -num-proc = 4 +num-proc = 3 service-addr = { host = "0.0.0.0", port = 8081 } #user = "nobody" #group = "nobody" @@ -33,6 +33,28 @@ 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" +slog-level = "debug" +log-level = "debug" + +[[raft.peers]] +host = "127.0.0.1" +port = 60151 +node-id = 1 + +[[raft.peers]] +host = "127.0.0.1" +port = 60152 +node-id = 2 + +[[raft.peers]] +host = "127.0.0.1" +port = 60153 +node-id = 3 + [docker-registry] ssl-verify = false diff --git a/python.lock b/python.lock index 933de779a10..1f85609f683 100644 --- a/python.lock +++ b/python.lock @@ -71,8 +71,10 @@ // "python-dotenv~=0.20.0", // "python-json-logger>=2.0.1", // "pyzmq~=24.0.1", +// "raftify==0.0.62", // "redis[hiredis]==4.5.5", // "rich~=12.2", +// "rraft-py==0.2.27", // "setproctitle~=1.3.2", // "tabulate~=0.8.9", // "tblib~=1.7", @@ -386,21 +388,21 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1160486b5ea96fcae6170cf2bdef029b9d3a283b7dbeabb3d7f1182769bfb6b7", - "url": "https://files.pythonhosted.org/packages/41/bd/0bd5c0547f2bf23a9260e12aaf363a40191eadecb44ef1bc5e926fb2942c/aioresponses-0.7.4-py2.py3-none-any.whl" + "hash": "d2c26defbb9b440ea2685ec132e90700907fd10bcca3e85ec2f157219f0d26f7", + "url": "https://files.pythonhosted.org/packages/e4/1e/c259a960a4dff46840133ce19682ef14db2922da80a6088283ec9c3f647a/aioresponses-0.7.6-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9b8c108b36354c04633bad0ea752b55d956a7602fe3e3234b939fc44af96f1d8", - "url": "https://files.pythonhosted.org/packages/7e/71/5524889790f7fc9385d8d71a92b5b4a927c652be0a216d89a217a07f5239/aioresponses-0.7.4.tar.gz" + "hash": "f795d9dbda2d61774840e7e32f5366f45752d1adc1b74c9362afd017296c7ee1", + "url": "https://files.pythonhosted.org/packages/ff/63/bb78ed078e2d514050aadc42a932465a83c43c628746f0e788500ec0bf5d/aioresponses-0.7.6.tar.gz" } ], "project_name": "aioresponses", "requires_dists": [ - "aiohttp<4.0.0,>=2.0.0" + "aiohttp<4.0.0,>=3.3.0" ], "requires_python": null, - "version": "0.7.4" + "version": "0.7.6" }, { "artifacts": [ @@ -513,13 +515,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f", - "url": "https://files.pythonhosted.org/packages/a2/8b/46919127496036c8e990b2b236454a0d8655fd46e1df2fd35610a9cbc842/alembic-1.12.0-py3-none-any.whl" + "hash": "47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb", + "url": "https://files.pythonhosted.org/packages/34/47/95d8f99c9f4a57079dfbcff5e023c5d81bde092d1c2354156340a56b3a1a/alembic-1.12.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b", - "url": "https://files.pythonhosted.org/packages/7d/bb/b254ca205628bfad1dbf4fe3826777b2638d74dd0c0b6ccc706d7a205def/alembic-1.12.0.tar.gz" + "hash": "bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f", + "url": "https://files.pythonhosted.org/packages/44/b4/253fe31261d9f5d603d89bd9e6fba1625494a6d761d319902dfe4db59016/alembic-1.12.1.tar.gz" } ], "project_name": "alembic", @@ -532,7 +534,7 @@ "typing-extensions>=4" ], "requires_python": ">=3.7", - "version": "1.12.0" + "version": "1.12.1" }, { "artifacts": [ @@ -559,6 +561,42 @@ "requires_python": null, "version": "9.0.1" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f", + "url": "https://files.pythonhosted.org/packages/85/4f/d010eca6914703d8e6be222165d02c3e708ed909cdb2b7af3743667f302e/anyio-4.1.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da", + "url": "https://files.pythonhosted.org/packages/6e/57/075e07fb01ae2b740289ec9daec670f60c06f62d04b23a68077fd5d73fab/anyio-4.1.0.tar.gz" + } + ], + "project_name": "anyio", + "requires_dists": [ + "Sphinx>=7; extra == \"doc\"", + "anyio[trio]; extra == \"test\"", + "coverage[toml]>=7; extra == \"test\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "exceptiongroup>=1.2.0; extra == \"test\"", + "hypothesis>=4.0; extra == \"test\"", + "idna>=2.8", + "packaging; extra == \"doc\"", + "psutil>=5.9; extra == \"test\"", + "pytest-mock>=3.6.1; extra == \"test\"", + "pytest>=7.0; extra == \"test\"", + "sniffio>=1.1", + "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", + "sphinx-rtd-theme; extra == \"doc\"", + "trio>=0.23; extra == \"trio\"", + "trustme; extra == \"test\"", + "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" + ], + "requires_python": ">=3.8", + "version": "4.1.0" + }, { "artifacts": [ { @@ -601,51 +639,72 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d7fa81ada2807bc50fea1dc741b26a4e99258825ba55913b0ddbf199a10d69d8", - "url": "https://files.pythonhosted.org/packages/b2/d7/d3b200875a6ade702c9c1cb14311b1a9481e8ed7b9a894b1b9d311638517/asyncpg-0.28.0-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "f8db604e37dabd43922d58f857817b1dfd8f88695b75c4cc1afe7ff1cc238a7b", + "url": "https://files.pythonhosted.org/packages/cf/b5/edd339d42867431d961f592c56f659275e6efe6245e8175039adc230c578/asyncclick-8.1.3.4-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "81d98cbf6c8813f9cd5599f586d56cfc532e9e6441391974d10827abb90fe833", + "url": "https://files.pythonhosted.org/packages/c4/14/989595a8cc0c475b571e0ef8b629fe658b8b42157732564dc5a7f6e73464/asyncclick-8.1.3.4.tar.gz" + } + ], + "project_name": "asyncclick", + "requires_dists": [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"" + ], + "requires_python": ">=3.7", + "version": "8.1.3.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b", + "url": "https://files.pythonhosted.org/packages/88/b0/6bebd69ed484055d47b78ea34fd9887c35694b63c9a648a7f02759d3bf73/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "99417210461a41891c4ff301490a8713d1ca99b694fef05dabd7139f9d64bd6c", - "url": "https://files.pythonhosted.org/packages/2d/89/25005cc5bd0089193e954de06cb993ff1a9590958c15ac67c412b06bb00a/asyncpg-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870", + "url": "https://files.pythonhosted.org/packages/27/25/d140bd503932f99528edc0a1461648973ad3c1c67f5929d11f3e8b5f81f4/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "f029c5adf08c47b10bcdc857001bbef551ae51c57b3110964844a9d79ca0f267", - "url": "https://files.pythonhosted.org/packages/77/a4/88069f7935b14c58534442a57be3299179eb46aace2d3c8716be199ff6a6/asyncpg-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac", + "url": "https://files.pythonhosted.org/packages/4a/13/f96284d7014dd06db2e78bea15706443d7895548bf74cf34f0c3ee1863fd/asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278", - "url": "https://files.pythonhosted.org/packages/a9/81/d86b6d6b6d643d9d3ea3926078965ae321b3aa1734a45ce8dca726a455f3/asyncpg-0.28.0.tar.gz" + "hash": "d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4", + "url": "https://files.pythonhosted.org/packages/69/28/3e3c4e243778f0361214b9d6e8bc6aa8e8bf55f35a2d2cb8949a6863caab/asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "b24e521f6060ff5d35f761a623b0042c84b9c9b9fb82786aadca95a9cb4a893b", - "url": "https://files.pythonhosted.org/packages/a9/dc/cbd7d8ce5671824b1f35d8b6d3b773e874d1afeaed73a0fbcee7def33a51/asyncpg-0.28.0-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e", + "url": "https://files.pythonhosted.org/packages/c1/11/7a6000244eaeb6b8ed2238bf33477c486515d6133f2c295913aca3ba4a00/asyncpg-0.29.0.tar.gz" }, { "algorithm": "sha256", - "hash": "ad1d6abf6c2f5152f46fff06b0e74f25800ce8ec6c80967f0bc789974de3c652", - "url": "https://files.pythonhosted.org/packages/c5/27/b3b1bd83c73c4836a5a994bc782d86ccf944da1d858885b71099e8da104c/asyncpg-0.28.0-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f", + "url": "https://files.pythonhosted.org/packages/c4/41/a0bdc18f13bdd5f27e7fc1b5de7e1caae19951967c109bca1a2e99cf3331/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a0e08fe2c9b3618459caaef35979d45f4e4f8d4f79490c9fa3367251366af207", - "url": "https://files.pythonhosted.org/packages/f3/5d/2b5f88592a75aa29b18d62f6d665457dd0df529b6a3317311b4e7b95f754/asyncpg-0.28.0-cp311-cp311-macosx_10_9_x86_64.whl" + "hash": "039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23", + "url": "https://files.pythonhosted.org/packages/f2/1f/1737248d7b1b75d19e7f07a98321bc58cb6fc979754c78544cfebff3359b/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl" } ], "project_name": "asyncpg", "requires_dists": [ "Sphinx~=5.3.0; extra == \"docs\"", - "flake8~=5.0; extra == \"test\"", + "async-timeout>=4.0.3; python_version < \"3.12.0\"", + "flake8~=6.1; extra == \"test\"", "sphinx-rtd-theme>=1.2.2; extra == \"docs\"", "sphinxcontrib-asyncio~=0.3.0; extra == \"docs\"", - "typing-extensions>=3.7.4.3; python_version < \"3.8\"", - "uvloop>=0.15.3; platform_system != \"Windows\" and extra == \"test\"" + "uvloop>=0.15.3; (platform_system != \"Windows\" and python_version < \"3.12.0\") and extra == \"test\"" ], - "requires_python": ">=3.7.0", - "version": "0.28.0" + "requires_python": ">=3.8.0", + "version": "0.29.0" }, { "artifacts": [ @@ -786,53 +845,53 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", - "url": "https://files.pythonhosted.org/packages/87/69/edacb37481d360d06fc947dab5734aaf511acb7d1a1f9e2849454376c0f8/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl" + "hash": "a7a7b8a87e51e5e8ca85b9fdaf3a5dc7aaf123365a09be7a27883d54b9a0c403", + "url": "https://files.pythonhosted.org/packages/36/7c/9fdf669fdc4392496818067861b2ec27c2622df4a355f9257360cb19154a/bcrypt-4.1.1-cp37-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", - "url": "https://files.pythonhosted.org/packages/41/16/49ff5146fb815742ad58cafb5034907aa7f166b1344d0ddd7fd1c818bd17/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "d573885b637815a7f3a3cd5f87724d7d0822da64b0ab0aa7f7c78bae534e86dc", + "url": "https://files.pythonhosted.org/packages/03/8e/d69af67a118aaae17a076d41b1b3f4400a66f39900b8cb72a0f918416f65/bcrypt-4.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", - "url": "https://files.pythonhosted.org/packages/64/fe/da28a5916128d541da0993328dc5cf4b43dfbf6655f2c7a2abe26ca2dc88/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl" + "hash": "f33b385c3e80b5a26b3a5e148e6165f873c1c202423570fdf45fe34e00e5f3e5", + "url": "https://files.pythonhosted.org/packages/1a/cc/ebf49d5d211d1ee622923c9196e6eea1274d1eecc8d00611f8b5f6f1d65a/bcrypt-4.1.1-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", - "url": "https://files.pythonhosted.org/packages/78/d4/3b2657bd58ef02b23a07729b0df26f21af97169dbd0b5797afa9e97ebb49/bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl" + "hash": "755b9d27abcab678e0b8fb4d0abdebeea1f68dd1183b3f518bad8d31fa77d8be", + "url": "https://files.pythonhosted.org/packages/8e/9b/870624ae1deb9cc997b0530fdd45292a6f272f80e024a023d0ea9d5e02e1/bcrypt-4.1.1-cp37-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", - "url": "https://files.pythonhosted.org/packages/7d/50/e683d8418974a602ba40899c8a5c38b3decaf5a4d36c32fc65dce454d8a8/bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl" + "hash": "bab33473f973e8058d1b2df8d6e095d237c49fbf7a02b527541a86a5d1dc4444", + "url": "https://files.pythonhosted.org/packages/a0/13/259124a851d361a2549560f9a3ccd286d17ef936017314a58cf7dffce8f7/bcrypt-4.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", - "url": "https://files.pythonhosted.org/packages/8c/ae/3af7d006aacf513975fd1948a6b4d6f8b4a307f8a244e1a3d3774b297aad/bcrypt-4.0.1.tar.gz" + "hash": "12f40f78dcba4aa7d1354d35acf45fae9488862a4fb695c7eeda5ace6aae273f", + "url": "https://files.pythonhosted.org/packages/af/82/96ffdbe0f56b12db0da8f1a9c869399d22231ed1313a84ea2ddc6381a498/bcrypt-4.1.1-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", - "url": "https://files.pythonhosted.org/packages/aa/48/fd2b197a9741fa790ba0b88a9b10b5e88e62ff5cf3e1bc96d8354d7ce613/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "fb931cd004a7ad36a89789caf18a54c20287ec1cd62161265344b9c4554fdb2e", + "url": "https://files.pythonhosted.org/packages/c5/8a/e7ba1562bfe80e9c480448f81118ad96087096ac9a36a57674bf8b520d69/bcrypt-4.1.1-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", - "url": "https://files.pythonhosted.org/packages/dd/4f/3632a69ce344c1551f7c9803196b191a8181c6a1ad2362c225581ef0d383/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl" + "hash": "df37f5418d4f1cdcff845f60e747a015389fa4e63703c918330865e06ad80007", + "url": "https://files.pythonhosted.org/packages/df/56/be5fda8e6fc05123c8c9f526095e93d0802a0a0b2beaf995ee2cc20aa2f8/bcrypt-4.1.1.tar.gz" }, { "algorithm": "sha256", - "hash": "08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", - "url": "https://files.pythonhosted.org/packages/ec/0a/1582790232fef6c2aa201f345577306b8bfe465c2c665dec04c86a016879/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl" + "hash": "2e197534c884336f9020c1f3a8efbaab0aa96fc798068cb2da9c671818b7fbb0", + "url": "https://files.pythonhosted.org/packages/e8/f0/5425ba170098cebff0a0c42b7e8ea8e5c5600fc4344cd058ef0bafc31a3e/bcrypt-4.1.1-cp37-abi3-macosx_13_0_universal2.whl" }, { "algorithm": "sha256", - "hash": "fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3", - "url": "https://files.pythonhosted.org/packages/fb/a7/ee4561fd9b78ca23c8e5591c150cc58626a5dfb169345ab18e1c2c664ee0/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl" + "hash": "2ade10e8613a3b8446214846d3ddbd56cfe9205a7d64742f0b75458c868f7492", + "url": "https://files.pythonhosted.org/packages/ff/c0/da85093fa0babf4fda1e31a1c8aab9026ee9e44539ecf706fe2a4e9391f1/bcrypt-4.1.1-cp37-abi3-musllinux_1_1_aarch64.whl" } ], "project_name": "bcrypt", @@ -840,8 +899,8 @@ "mypy; extra == \"typecheck\"", "pytest!=3.3.0,>=3.2.1; extra == \"tests\"" ], - "requires_python": ">=3.6", - "version": "4.0.1" + "requires_python": ">=3.7", + "version": "4.1.1" }, { "artifacts": [ @@ -871,48 +930,48 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "ff3d0116e0ca6c096547652390025780eace3a28f6c04c9ffbf38448f1e5a87b", - "url": "https://files.pythonhosted.org/packages/c7/dd/4fe47b2cec8731ec26d7410e659c4f0c4cd36baa835e2312cb0ec5383b07/boto3-1.28.65-py3-none-any.whl" + "hash": "fc7c0dd5fa74ae0d57e11747695bdba4ad164e62dee35db15b43762c392fbd92", + "url": "https://files.pythonhosted.org/packages/c5/d3/3210735b9ab42042b76d5ddaf33e88cee8e08827eb078ebba00d1fda863a/boto3-1.33.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9d52a1605657aeb5b19b09cfc01d9a92f88a616a5daf5479a59656d6341ea6b3", - "url": "https://files.pythonhosted.org/packages/1b/2f/4ccd05e765a9aa3222125da37ceced40b4133094069c4d011ca7ae37681f/boto3-1.28.65.tar.gz" + "hash": "70626598dd6698d6da8f2854a1ae5010f175572e2a465b2aa86685c745c1013c", + "url": "https://files.pythonhosted.org/packages/9d/4d/09a3eb00e6d017dafae80c6ea307992263405aad315587e0b63864ae97e5/boto3-1.33.2.tar.gz" } ], "project_name": "boto3", "requires_dists": [ - "botocore<1.32.0,>=1.31.65", + "botocore<1.34.0,>=1.33.2", "botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", - "s3transfer<0.8.0,>=0.7.0" + "s3transfer<0.9.0,>=0.8.0" ], "requires_python": ">=3.7", - "version": "1.28.65" + "version": "1.33.2" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "f74e3da98dfcec17bc63ef58f82c643bf5bd7ec6cc11a26ede21cc4cd064917f", - "url": "https://files.pythonhosted.org/packages/63/c6/8e29a2b9dffa188d07c26d19ae578a26d8063834e4d844bf22c2a0028229/botocore-1.31.65-py3-none-any.whl" + "hash": "5c46b7e8450efbf7ddc2a0016eee7225a5564583122e25a20ca92a29a105225c", + "url": "https://files.pythonhosted.org/packages/86/1c/35a825f86ea910a86b5d54461902b760b868dec9267e4964a697424301d0/botocore-1.33.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "90716c6f1af97e5c2f516e9a3379767ebdddcc6cbed79b026fa5038ce4e5e43e", - "url": "https://files.pythonhosted.org/packages/42/30/e5e2126eca77baedbf51e48241c898d99784d272bcf2fb47f5a10360e555/botocore-1.31.65.tar.gz" + "hash": "16a30faac6e6f17961c009defb74ab1a3508b8abc58fab98e7cf96af0d91ea84", + "url": "https://files.pythonhosted.org/packages/d5/73/40c9dd27acb7fad5d13259c406d305e4452f927d1b1dd16eee79586f5f9c/botocore-1.33.2.tar.gz" } ], "project_name": "botocore", "requires_dists": [ - "awscrt==0.16.26; extra == \"crt\"", + "awscrt==0.19.17; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "python-dateutil<3.0.0,>=2.1", "urllib3<1.27,>=1.25.4; python_version < \"3.10\"", "urllib3<2.1,>=1.25.4; python_version >= \"3.10\"" ], "requires_python": ">=3.7", - "version": "1.31.65" + "version": "1.33.2" }, { "artifacts": [ @@ -1004,19 +1063,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9", - "url": "https://files.pythonhosted.org/packages/4c/dd/2234eab22353ffc7d94e8d13177aaa050113286e93e7b40eae01fbf7c3d9/certifi-2023.7.22-py3-none-any.whl" + "hash": "e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474", + "url": "https://files.pythonhosted.org/packages/64/62/428ef076be88fa93716b576e4a01f919d25968913e817077a386fcbe4f42/certifi-2023.11.17-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "url": "https://files.pythonhosted.org/packages/98/98/c2ff18671db109c9f10ed27f5ef610ae05b73bd876664139cf95bd1429aa/certifi-2023.7.22.tar.gz" + "hash": "9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "url": "https://files.pythonhosted.org/packages/d4/91/c89518dd4fe1f3a4e3f6ab7ff23cb00ef2e8c9adf99dacc618ad5e068e28/certifi-2023.11.17.tar.gz" } ], "project_name": "certifi", "requires_dists": [], "requires_python": ">=3.6", - "version": "2023.7.22" + "version": "2023.11.17" }, { "artifacts": [ @@ -1082,84 +1141,84 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2", - "url": "https://files.pythonhosted.org/packages/a3/dc/efab5b27839f04be4b8058c1eb85b7ab7dbc55ef8067250bea0518392756/charset_normalizer-3.3.0-py3-none-any.whl" + "hash": "3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "url": "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa", - "url": "https://files.pythonhosted.org/packages/07/f3/6149137d06829d1d8b566421a194b9a98d593fb63a1c0d701813ae58bc80/charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "url": "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c", - "url": "https://files.pythonhosted.org/packages/2a/1f/199f8716d730157a60ba2574c38045a30e15df288f5de5abbdb1e1b0e53d/charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "url": "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c", - "url": "https://files.pythonhosted.org/packages/50/5f/b440775f1abaef7f493f0fa051ce1db5903d66cc5515e1a376c71e161cc5/charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl" + "hash": "eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "url": "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl" }, { "algorithm": "sha256", - "hash": "f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", - "url": "https://files.pythonhosted.org/packages/56/d9/0bcd68d787acc894c5ddae42559f69b00ff594d8cd8afd7b8e3dda3450ad/charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "url": "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl" }, { "algorithm": "sha256", - "hash": "5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34", - "url": "https://files.pythonhosted.org/packages/5a/89/0bbdf76aacc2fa9952757c4bac30915cf0c32ce6f15ccb93b70cf8b2fad9/charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl" + "hash": "80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "url": "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63", - "url": "https://files.pythonhosted.org/packages/75/e5/038cb532b4f30f45aa3c6cca2fd4181b25cc9f9c8bb0b1792097d645a25a/charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl" + "hash": "573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "url": "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1", - "url": "https://files.pythonhosted.org/packages/7d/ca/d937d0c175cac51b7da9e7167d57685f908a89b01c8d4bc4950af1cd31fa/charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "url": "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382", - "url": "https://files.pythonhosted.org/packages/88/64/f460ff3ec5c7d4e016f90b7bb04791b6ce5d7760e9ffa463f27c21a55e98/charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "url": "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz" }, { "algorithm": "sha256", - "hash": "380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459", - "url": "https://files.pythonhosted.org/packages/8b/fa/6e9cff7551dc3fc052c065ae319736a502415eee9b5dce2528094e672ec0/charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "url": "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078", - "url": "https://files.pythonhosted.org/packages/be/86/a00981046d56e006f0acaa96392e7d09693be44914eac3435c6e86a6faaa/charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl" + "hash": "1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "url": "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6", - "url": "https://files.pythonhosted.org/packages/cf/ac/e89b2f2f75f51e9859979b56d2ec162f7f893221975d244d8d5277aa9489/charset-normalizer-3.3.0.tar.gz" + "hash": "deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "url": "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e", - "url": "https://files.pythonhosted.org/packages/d3/46/76bf2f07edb024c891b1c66d6f3f709093deec314f78307662bb83a33390/charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl" + "hash": "e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "url": "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05", - "url": "https://files.pythonhosted.org/packages/e7/37/5f9cd08268f1e1fde2ab8c0a42a0ff596f26a74fa25f7df00b66cb0e40af/charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "url": "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293", - "url": "https://files.pythonhosted.org/packages/ff/b6/9222090f396f33cd58aa5b08b9bbf8871416b746a0c7b412a41a973674a5/charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "url": "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" } ], "project_name": "charset-normalizer", "requires_dists": [], "requires_python": ">=3.7.0", - "version": "3.3.0" + "version": "3.3.2" }, { "artifacts": [ @@ -1247,48 +1306,48 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", - "url": "https://files.pythonhosted.org/packages/ae/8e/c2466577a0f29421a74c5e0c7731274c3f82504e0dd08a3ef0489822f0cd/cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl" + "hash": "49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", + "url": "https://files.pythonhosted.org/packages/c5/07/826d66b6b03c5bfde8b451bea22c41e68d60aafff0ffa02c5f0819844319/cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", - "url": "https://files.pythonhosted.org/packages/06/5d/f992c40471b60b762dca2b118c0a7837e446bea917f2be54b8f49802fe5e/cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl" + "hash": "841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", + "url": "https://files.pythonhosted.org/packages/14/fd/dd5bd6ab0d12476ebca579cbfd48d31bd90fa28fa257b209df585dcf62a0/cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", - "url": "https://files.pythonhosted.org/packages/25/1d/f86ce362aedc580c3f90c0d74fa097289e3af9ba52b8d5a37369c186b0f1/cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", + "url": "https://files.pythonhosted.org/packages/3e/81/ae2c51ea2b80d57d5756a12df67816230124faea0a762a7a6304fe3c819c/cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", - "url": "https://files.pythonhosted.org/packages/a2/5c/b821ad3b2f1506b8042500edfb671c30efb6eca7dc5aa63236342338669f/cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl" + "hash": "43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", + "url": "https://files.pythonhosted.org/packages/62/bd/69628ab50368b1beb900eb1de5c46f8137169b75b2458affe95f2f470501/cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", - "url": "https://files.pythonhosted.org/packages/a2/d0/b8cf2c1367f850011d4618348760b23bb1268efba9e6ca03c063803e8763/cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl" + "hash": "5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", + "url": "https://files.pythonhosted.org/packages/68/bb/475658ea92653a894589e657d6cea9ae01354db73405d62126ac5e74e2f8/cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", - "url": "https://files.pythonhosted.org/packages/bb/c1/e8ca19a3e9ac5c867efa6f23ce0b119ad00a16b6019e49a298b8c1fe6866/cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl" + "hash": "928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", + "url": "https://files.pythonhosted.org/packages/a9/76/d705397d076fcbf5671544eb72a70b5a5ac83462d23dbd2a365a3bf3692a/cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", - "url": "https://files.pythonhosted.org/packages/e2/b5/11bcc59ad5a7121fe1279a716f3e212f6b37ef993726b06c25aa3aefa0a7/cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", + "url": "https://files.pythonhosted.org/packages/b6/4a/1808333c5ea79cb6d51102036cbcf698704b1fc7a5ccd139957aeadd2311/cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", - "url": "https://files.pythonhosted.org/packages/eb/4b/f86cc66c632cf0948ca1712aadd255f624deef1cd371ea3bfd30851e188d/cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl" + "hash": "13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", + "url": "https://files.pythonhosted.org/packages/ce/b3/13a12ea7edb068de0f62bac88a8ffd92cc2901881b391839851846b84a81/cryptography-41.0.7.tar.gz" }, { "algorithm": "sha256", - "hash": "7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", - "url": "https://files.pythonhosted.org/packages/ef/33/87512644b788b00a250203382e40ee7040ae6fa6b4c4a31dcfeeaa26043b/cryptography-41.0.4.tar.gz" + "hash": "3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", + "url": "https://files.pythonhosted.org/packages/e4/73/5461318abd2fe426855a2f66775c063bbefd377729ece3c3ee048ddf19a5/cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl" } ], "project_name": "cryptography", @@ -1314,7 +1373,7 @@ "twine>=1.12.0; extra == \"docstest\"" ], "requires_python": ">=3.7", - "version": "41.0.4" + "version": "41.0.7" }, { "artifacts": [ @@ -1488,13 +1547,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda", - "url": "https://files.pythonhosted.org/packages/39/7c/2e4fa55a99f83ef9ef229ac5d59c44ceb90e2d0145711590c0fa39669f32/google_auth-2.23.3-py2.py3-none-any.whl" + "hash": "d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2", + "url": "https://files.pythonhosted.org/packages/86/a7/75911c13a242735d5aeaca6a272da380335ff4ba5f26d6b2ae20ff682d13/google_auth-2.23.4-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3", - "url": "https://files.pythonhosted.org/packages/45/71/0f19d6f51b6ea291fc8f179d152d675f49acf88cb44f743b37bf51ef2ec1/google-auth-2.23.3.tar.gz" + "hash": "79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3", + "url": "https://files.pythonhosted.org/packages/f9/ff/06d757a319b551bccd70772dc656dd0bdedec54e72e407bdd6162116cb3a/google-auth-2.23.4.tar.gz" } ], "project_name": "google-auth", @@ -1512,7 +1571,7 @@ "rsa<5,>=3.1.4" ], "requires_python": ">=3.7", - "version": "2.23.3" + "version": "2.23.4" }, { "artifacts": [ @@ -1603,48 +1662,48 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362", - "url": "https://files.pythonhosted.org/packages/51/b0/0bcd4c699e4f084d1dab66036816b7af8badba297027d846f939d62d09b9/greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94", + "url": "https://files.pythonhosted.org/packages/ce/76/257d50829841cb13b163764cdef35197c8a0bd351ad94fc05795ca28fb21/greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c", - "url": "https://files.pythonhosted.org/packages/48/de/814c858b701dee063d4728ad6850246eabf355ff1f0c35429871f5b5b1a0/greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65", + "url": "https://files.pythonhosted.org/packages/3b/20/da6746e1efbb114740b6e1671ee0d35a5ff39e49f6a1c169e8328d47b7c8/greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b", - "url": "https://files.pythonhosted.org/packages/6c/df/1e3e52e35e56b912c7bcd64ba2010d6972c43dff96794074b32a62345970/greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" + "hash": "19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec", + "url": "https://files.pythonhosted.org/packages/3e/87/88d45172c2fe19052d782bf616ce5a2a92604823320b7cd59ea2dd9ad41d/greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd", - "url": "https://files.pythonhosted.org/packages/93/86/1e6b425df2340988f257361c8cfe1ab394dbcf4749dc7bf2e9be85241205/greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96", + "url": "https://files.pythonhosted.org/packages/42/85/32e38abd5f046d56c9ff762c66ddd763cee17daccefa6f22fdae7f7e6472/greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7", - "url": "https://files.pythonhosted.org/packages/a0/fb/3eeb54137cc4d01248babb62fd12c7a5faba36b13692ed622ea43fd4d648/greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl" + "hash": "816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b", + "url": "https://files.pythonhosted.org/packages/54/df/718c9b3e90edba70fa919bb3aaa5c3c8dabf3a8252ad1e93d33c348e5ca4/greenlet-3.0.1.tar.gz" }, { "algorithm": "sha256", - "hash": "19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b", - "url": "https://files.pythonhosted.org/packages/b6/02/47dbd5e1c9782e6d3f58187fa10789e308403f3fc3a490b3646b2bff6d9f/greenlet-3.0.0.tar.gz" + "hash": "2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234", + "url": "https://files.pythonhosted.org/packages/5b/ee/3b61723db7690e1168f4ed1af98ea595bcc843c6221d13846d6cc390b2cb/greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e", - "url": "https://files.pythonhosted.org/packages/c9/8e/8ff2d2b6527130833d94dba5e83bf8e5f032234e9670bb391c4638858b13/greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884", + "url": "https://files.pythonhosted.org/packages/6b/bd/033343cf60d27702d3be9edba9dbc8392594e6c4a6eede337dbb40e0c4b2/greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c", - "url": "https://files.pythonhosted.org/packages/d1/8b/564ef37f2d93067190ad634a44ad2398028e3367cd3058a4c38e8b9715dd/greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a", + "url": "https://files.pythonhosted.org/packages/b1/62/1501a7dd0ac305a3f2c4d5ac9e526a71e96070cb1c27a6d2d7fd11c65d38/greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14", - "url": "https://files.pythonhosted.org/packages/de/03/afb172d11fb95d17be9b16daf7f0be02235485b7879c9b65802c087610d4/greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72", + "url": "https://files.pythonhosted.org/packages/b7/c1/bf937378fd918599a3b51f55bf049e5df59eac6557380a30f3e78da56b7e/greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } ], "project_name": "greenlet", @@ -1654,7 +1713,7 @@ "psutil; extra == \"test\"" ], "requires_python": ">=3.7", - "version": "3.0.0" + "version": "3.0.1" }, { "artifacts": [ @@ -1862,13 +1921,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "8bc9e2bb9315e61ec06bf690151ae35aeb65651ab091266941edf97c90836404", - "url": "https://files.pythonhosted.org/packages/4a/52/cccfc7a0d3bcf52cca6f6e1792786075df979346d638bf4cf5bc8bf2be3c/humanize-4.8.0-py3-none-any.whl" + "hash": "ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16", + "url": "https://files.pythonhosted.org/packages/aa/2b/2ae0c789fd08d5b44e745726d08a17e6d3d7d09071d05473105edc7615f2/humanize-4.9.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9783373bf1eec713a770ecaa7c2d7a7902c98398009dfa3d8a2df91eec9311e8", - "url": "https://files.pythonhosted.org/packages/0c/84/e58c665f4ebb03d2fbeb28b51afb0743f846db18a5b594ed8b8973676ddf/humanize-4.8.0.tar.gz" + "hash": "582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa", + "url": "https://files.pythonhosted.org/packages/76/21/7a0b24fae849562397efd79da58e62437243ae0fd0f6c09c6bc26225b75c/humanize-4.9.0.tar.gz" } ], "project_name": "humanize", @@ -1878,25 +1937,25 @@ "pytest; extra == \"tests\"" ], "requires_python": ">=3.8", - "version": "4.8.0" + "version": "4.9.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2", - "url": "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl" + "hash": "c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", + "url": "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "url": "https://files.pythonhosted.org/packages/8b/e1/43beb3d38dba6cb420cefa297822eac205a277ab43e5ba5d5c46faf96438/idna-3.4.tar.gz" + "hash": "9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "url": "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz" } ], "project_name": "idna", "requires_dists": [], "requires_python": ">=3.5", - "version": "3.4" + "version": "3.6" }, { "artifacts": [ @@ -2019,13 +2078,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "6a2a950ec23a8f62f9e4c66acec7f0ea6c7d1f80ba0992e747b10c56ce2e6dbe", - "url": "https://files.pythonhosted.org/packages/dc/05/e91a1a935a25ca1b46c78260def39125b2cfca96c2adbc285d365af23e3f/jupyter_client-8.4.0-py3-none-any.whl" + "hash": "909c474dbe62582ae62b758bca86d6518c85234bdee2d908c778db6d72f39d99", + "url": "https://files.pythonhosted.org/packages/43/ae/5f4f72980765e2e5e02b260f9c53bcc706cefa7ac9c8d7240225c55788d4/jupyter_client-8.6.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "dc1b857d5d7d76ac101766c6e9b646bf18742721126e72e5d484c75a993cada2", - "url": "https://files.pythonhosted.org/packages/77/d8/1627e06db1bd3b6907ee43eae38651848de6e57996370fff60c273366509/jupyter_client-8.4.0.tar.gz" + "hash": "0642244bb83b4764ae60d07e010e15f0e2d275ec4e918a8f7b80fbbef3ca60c7", + "url": "https://files.pythonhosted.org/packages/71/04/4418fca04fd65a26771113a0a46220a1a54a6d6bcc6fae4ad6b69eb27dd5/jupyter_client-8.6.0.tar.gz" } ], "project_name": "jupyter-client", @@ -2054,19 +2113,19 @@ "traitlets>=5.3" ], "requires_python": ">=3.8", - "version": "8.4.0" + "version": "8.6.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "66e252f675ac04dcf2feb6ed4afb3cd7f68cf92f483607522dc251f32d471571", - "url": "https://files.pythonhosted.org/packages/ac/92/bec527b68e2b56d0b1a30db19ce8370cba69fb68d34c981f4549564ca551/jupyter_core-5.4.0-py3-none-any.whl" + "hash": "e11e02cd8ae0a9de5c6c44abf5727df9f2581055afe00b22183f621ba3585805", + "url": "https://files.pythonhosted.org/packages/ab/ea/af6508f71d2bcbf4db538940120cc3d3f10287f62105e756bd315aa345b5/jupyter_core-5.5.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e4b98344bb94ee2e3e6c4519a97d001656009f9cb2b7f2baf15b3c205770011d", - "url": "https://files.pythonhosted.org/packages/cc/df/ac7f3eba596110143561c1c9d57f288cf2df69643c9daf211c5f9c2dd85d/jupyter_core-5.4.0.tar.gz" + "hash": "880b86053bf298a8724994f95e99b99130659022a4f7f45f563084b6223861d3", + "url": "https://files.pythonhosted.org/packages/5c/3d/c75bda485eaf15cd430383deb0c441aa822679ea88c5b32cfc2013f678e1/jupyter_core-5.5.0.tar.gz" } ], "project_name": "jupyter-core", @@ -2075,6 +2134,7 @@ "myst-parser; extra == \"docs\"", "platformdirs>=2.5", "pre-commit; extra == \"test\"", + "pydata-sphinx-theme; extra == \"docs\"", "pytest-cov; extra == \"test\"", "pytest-timeout; extra == \"test\"", "pytest; extra == \"test\"", @@ -2086,7 +2146,7 @@ "traitlets>=5.3" ], "requires_python": ">=3.8", - "version": "5.4.0" + "version": "5.5.0" }, { "artifacts": [ @@ -2144,13 +2204,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9e5dc5bbf93fa1840083707285262514a0ef8a6613874af7ea1cec60468d6e92", - "url": "https://files.pythonhosted.org/packages/9a/16/54258d6b368db265bde53515fe7ccb7c261b4ff3591b6354531abfe922ef/lark-1.1.7-py3-none-any.whl" + "hash": "7d2c221a66a8165f3f81aacb958d26033d40d972fdb70213ab0a2e0627e29c86", + "url": "https://files.pythonhosted.org/packages/99/ca/f3532a61dce7dd52fbd38737a12e16cdc7699697e23287eb7addfdd93e3f/lark-1.1.8-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "be7437bf1f37ab08b355f29ff2571d77d777113d0a8c4352b0c513dced6c5a1e", - "url": "https://files.pythonhosted.org/packages/85/70/4465b0b7dc6ea72cc2c4ea25a2c6ad62cca7918eda030db36a4c11f6f5d9/lark-1.1.7.tar.gz" + "hash": "7ef424db57f59c1ffd6f0d4c2b705119927f566b68c0fe1942dddcc0e44391a5", + "url": "https://files.pythonhosted.org/packages/12/1c/b466b58dacac15ffefce9bcb5128e18948a143849610a7d5300f31920be0/lark-1.1.8.tar.gz" } ], "project_name": "lark", @@ -2161,31 +2221,53 @@ "regex; extra == \"regex\"" ], "requires_python": ">=3.6", - "version": "1.1.7" + "version": "1.1.8" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "url": "https://files.pythonhosted.org/packages/03/3b/68690a035ba7347860f1b8c0cde853230ba69ff41df5884ea7d89fe68cd3/Mako-1.2.4-py3-none-any.whl" + "hash": "01f8e42c364cf33c7dd52c1f2cb67729e8cd384db1269db8a70486c6d257afdf", + "url": "https://files.pythonhosted.org/packages/5f/70/fe9fe907a31b97f50d0b9a968b7c45d5fa98d23514f5550ac80ff91afeee/lmdb-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "323cdee3c11512f42de53baaf732080fe0ee170f0f3b7818a427989dd7bd1259", + "url": "https://files.pythonhosted.org/packages/8e/5b/3c20cf88c1626dff85f06218becc3da6f3c9cb86b1b9a746e3431d0a1939/lmdb-1.4.0-cp311-cp311-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34", - "url": "https://files.pythonhosted.org/packages/05/5f/2ba6e026d33a0e6ddc1dddf9958677f76f5f80c236bd65309d280b166d3e/Mako-1.2.4.tar.gz" + "hash": "39f6c4ee145d28d17025d350720abb6f95db816514e868db57444fdef51cbb47", + "url": "https://files.pythonhosted.org/packages/fd/78/4cdc5927d5f3c3c86c4da0108c2eeba544cd67e773232164d59f3e442ff0/lmdb-1.4.0.tar.gz" + } + ], + "project_name": "lmdb", + "requires_dists": [], + "requires_python": null, + "version": "1.4.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9", + "url": "https://files.pythonhosted.org/packages/24/3b/11fe92d68c6a42468ddab0cf03f454419b0788fff4e91ba46b8bebafeffd/Mako-1.3.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b", + "url": "https://files.pythonhosted.org/packages/a9/6e/6b41e654bbdcef90c6b9e7f280bf7cbd756dc2560ce76214f5cdbc4ddab5/Mako-1.3.0.tar.gz" } ], "project_name": "mako", "requires_dists": [ "Babel; extra == \"babel\"", "MarkupSafe>=0.9.2", - "importlib-metadata; python_version < \"3.8\"", "lingua; extra == \"lingua\"", "pytest; extra == \"testing\"" ], - "requires_python": ">=3.7", - "version": "1.2.4" + "requires_python": ">=3.8", + "version": "1.3.0" }, { "artifacts": [ @@ -2579,13 +2661,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "url": "https://files.pythonhosted.org/packages/39/7b/88dbb785881c28a102619d46423cb853b46dbccc70d3ac362d99773a78ce/pexpect-4.8.0-py2.py3-none-any.whl" + "hash": "7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "url": "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c", - "url": "https://files.pythonhosted.org/packages/e5/9b/ff402e0e930e70467a7178abb7c128709a30dfb22d8777c043e501bc1b10/pexpect-4.8.0.tar.gz" + "hash": "ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", + "url": "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz" } ], "project_name": "pexpect", @@ -2593,19 +2675,19 @@ "ptyprocess>=0.5" ], "requires_python": null, - "version": "4.8.0" + "version": "4.9.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e", - "url": "https://files.pythonhosted.org/packages/56/29/3ec311dc18804409ecf0d2b09caa976f3ae6215559306b5b530004e11156/platformdirs-3.11.0-py3-none-any.whl" + "hash": "118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "url": "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "url": "https://files.pythonhosted.org/packages/d3/e3/aa14d6b2c379fbb005993514988d956f1b9fdccd9cbe78ec0dbe5fb79bf5/platformdirs-3.11.0.tar.gz" + "hash": "cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", + "url": "https://files.pythonhosted.org/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz" } ], "project_name": "platformdirs", @@ -2622,7 +2704,7 @@ "typing-extensions>=4.7.1; python_version < \"3.8\"" ], "requires_python": ">=3.7", - "version": "3.11.0" + "version": "4.0.0" }, { "artifacts": [ @@ -2651,13 +2733,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88", - "url": "https://files.pythonhosted.org/packages/a9/b4/ba77c84edf499877317225d7b7bc047a81f7c2eed9628eeb6bab0ac2e6c9/prompt_toolkit-3.0.39-py3-none-any.whl" + "hash": "f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2", + "url": "https://files.pythonhosted.org/packages/1f/9d/be9b01085bbd67a71c4b6aa02518fade8104e7a2224e5de5e947811d7176/prompt_toolkit-3.0.41-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac", - "url": "https://files.pythonhosted.org/packages/9a/02/76cadde6135986dc1e82e2928f35ebeb5a1af805e2527fe466285593a2ba/prompt_toolkit-3.0.39.tar.gz" + "hash": "941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0", + "url": "https://files.pythonhosted.org/packages/d9/7b/7d88d94427e1e179e0a62818e68335cf969af5ca38033c0ca02237ab6ee7/prompt_toolkit-3.0.41.tar.gz" } ], "project_name": "prompt-toolkit", @@ -2665,7 +2747,7 @@ "wcwidth" ], "requires_python": ">=3.7.0", - "version": "3.0.39" + "version": "3.0.41" }, { "artifacts": [ @@ -2779,19 +2861,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", - "url": "https://files.pythonhosted.org/packages/14/e5/b56a725cbde139aa960c26a1a3ca4d4af437282e20b5314ee6a3501e7dfc/pyasn1-0.5.0-py2.py3-none-any.whl" + "hash": "4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58", + "url": "https://files.pythonhosted.org/packages/d1/75/4686d2872bf2fc0b37917cbc8bbf0dd3a5cdb0990799be1b9cbf1e1eb733/pyasn1-0.5.1-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde", - "url": "https://files.pythonhosted.org/packages/61/ef/945a8bcda7895717c8ba4688c08a11ef6454f32b8e5cb6e352a9004ee89d/pyasn1-0.5.0.tar.gz" + "hash": "6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c", + "url": "https://files.pythonhosted.org/packages/ce/dc/996e5446a94627fe8192735c20300ca51535397e31e7097a3cc80ccf78b7/pyasn1-0.5.1.tar.gz" } ], "project_name": "pyasn1", "requires_dists": [], "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", - "version": "0.5.0" + "version": "0.5.1" }, { "artifacts": [ @@ -2944,21 +3026,22 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692", - "url": "https://files.pythonhosted.org/packages/43/88/29adf0b44ba6ac85045e63734ae0997d3c58d8b1a91c914d240828d0d73d/Pygments-2.16.1-py3-none-any.whl" + "hash": "b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "url": "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29", - "url": "https://files.pythonhosted.org/packages/d6/f7/4d461ddf9c2bcd6a4d7b2b139267ca32a69439387cc1f02a924ff8883825/Pygments-2.16.1.tar.gz" + "hash": "da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", + "url": "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz" } ], "project_name": "pygments", "requires_dists": [ + "colorama>=0.4.6; extra == \"windows-terminal\"", "importlib-metadata; python_version < \"3.8\" and extra == \"plugins\"" ], "requires_python": ">=3.7", - "version": "2.16.1" + "version": "2.17.2" }, { "artifacts": [ @@ -2997,13 +3080,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", - "url": "https://files.pythonhosted.org/packages/df/d0/e192c4275aecabf74faa1aacd75ef700091913236ec78b1a98f62a2412ee/pytest-7.4.2-py3-none-any.whl" + "hash": "0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "url": "https://files.pythonhosted.org/packages/f3/8c/f16efd81ca8e293b2cc78f111190a79ee539d0d5d36ccd49975cb3beac60/pytest-7.4.3-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069", - "url": "https://files.pythonhosted.org/packages/e5/d0/18209bb95db8ee693a9a04fe056ab0663c6d6b1baf67dd50819dd9cd4bd7/pytest-7.4.2.tar.gz" + "hash": "d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5", + "url": "https://files.pythonhosted.org/packages/38/d4/174f020da50c5afe9f5963ad0fc5b56a4287e3586e3de5b3c8bce9c547b4/pytest-7.4.3.tar.gz" } ], "project_name": "pytest", @@ -3026,7 +3109,7 @@ "xmlschema; extra == \"testing\"" ], "requires_python": ">=3.7", - "version": "7.4.2" + "version": "7.4.3" }, { "artifacts": [ @@ -3218,6 +3301,41 @@ "requires_python": ">=3.6", "version": "24.0.1" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "510add36f705a4cb2d6ffa05ebc6fd930785c2fe2204f8dd9c43e26a46ac6f8f", + "url": "https://files.pythonhosted.org/packages/e5/ad/01f47f0c63d64a857a9a9014912500ea662484bf0c39a0ace3bbb40fb948/raftify-0.0.62-py3-none-any.whl" + } + ], + "project_name": "raftify", + "requires_dists": [ + "aiohttp==3.8.4; extra == \"dev\"", + "anyio>=4.0.0", + "argparse==1.4.0; extra == \"dev\"", + "asyncclick>=8.1.3.4", + "black; extra == \"dev\"", + "colorlog; extra == \"dev\"", + "flake8; extra == \"dev\"", + "grpcio-tools>=1.56.2", + "grpcio>=1.56.2", + "isort; extra == \"dev\"", + "lmdb==1.4.0", + "mypy; extra == \"dev\"", + "protobuf>=4.23.4", + "pytest-asyncio~=0.19.0; extra == \"dev\"", + "pytest~=7.1.2; extra == \"dev\"", + "rraft-py>=0.2.27", + "tabulate>=0.8.10", + "tomli~=2.0.1; extra == \"dev\"", + "twine~=4.0.1; extra == \"build\"", + "types-protobuf; extra == \"dev\"", + "wheel>=0.41.1; extra == \"build\"" + ], + "requires_python": ">=3.10", + "version": "0.0.62" + }, { "artifacts": [ { @@ -3335,6 +3453,24 @@ "requires_python": "<4.0.0,>=3.6.3", "version": "12.6.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "93a9fb9095213cf5b530b884dd9a719999a1289ea4dc4bd0d89cb5a1e4bb7134", + "url": "https://files.pythonhosted.org/packages/76/1a/191c1014ae0827f509b7bd7a5cad6d29bf3a966ad46bc554c49dfb589fd5/rraft_py-0.2.27-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "df77d268a95d366449017d7ab2109c3720ed8486c3434b025252ca03553b2685", + "url": "https://files.pythonhosted.org/packages/4c/5d/33ffac8b3623a1d08831e90e1f73cb7ee741c5113bf8397249119b7bd875/rraft_py-0.2.27.tar.gz" + } + ], + "project_name": "rraft-py", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "0.2.27" + }, { "artifacts": [ { @@ -3359,22 +3495,22 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", - "url": "https://files.pythonhosted.org/packages/5a/4b/fec9ce18f8874a96c5061422625ba86c3ee1e6587ccd92ff9f5bf7bd91b2/s3transfer-0.7.0-py3-none-any.whl" + "hash": "d1c52af7bceca1650d0f27728b29bb4925184aead7b55bccacf893b79a108604", + "url": "https://files.pythonhosted.org/packages/eb/d0/ad1680cdc280f095f3b39b954ab9fd093628282900d4308740737def27b9/s3transfer-0.8.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e", - "url": "https://files.pythonhosted.org/packages/3f/ff/5fd9375f3fe467263cff9cad9746fd4c4e1399440ea9563091c958ff90b5/s3transfer-0.7.0.tar.gz" + "hash": "e6cafd5643fc7b44fddfba1e5b521005675b0e07533ddad958a3554bc87d7330", + "url": "https://files.pythonhosted.org/packages/d3/8c/babd90ebb61a8ce1ade0dc1f87e067287f7d97bf84d5ded1c4cc3fed5134/s3transfer-0.8.1.tar.gz" } ], "project_name": "s3transfer", "requires_dists": [ - "botocore<2.0a.0,>=1.12.36", - "botocore[crt]<2.0a.0,>=1.20.29; extra == \"crt\"" + "botocore<2.0a.0,>=1.33.2", + "botocore[crt]<2.0a.0,>=1.33.2; extra == \"crt\"" ], "requires_python": ">=3.7", - "version": "0.7.0" + "version": "0.8.1" }, { "artifacts": [ @@ -3445,13 +3581,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a", - "url": "https://files.pythonhosted.org/packages/bb/26/7945080113158354380a12ce26873dd6c1ebd88d47f5bc24e2c5bb38c16a/setuptools-68.2.2-py3-none-any.whl" + "hash": "1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2", + "url": "https://files.pythonhosted.org/packages/bb/e1/ed2dd0850446b8697ad28d118df885ad04140c64ace06c4bd559f7c8a94f/setuptools-69.0.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "url": "https://files.pythonhosted.org/packages/ef/cc/93f7213b2ab5ed383f98ce8020e632ef256b406b8569606c3f160ed8e1c9/setuptools-68.2.2.tar.gz" + "hash": "735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6", + "url": "https://files.pythonhosted.org/packages/4b/d9/d0cf66484b7e28a9c42db7e3929caed46f8b80478cd8c9bd38b7be059150/setuptools-69.0.2.tar.gz" } ], "project_name": "setuptools", @@ -3488,11 +3624,11 @@ "pytest>=6; extra == \"testing\"", "rst.linker>=1.9; extra == \"docs\"", "sphinx-favicon; extra == \"docs\"", - "sphinx-hoverxref<2; extra == \"docs\"", "sphinx-inline-tabs; extra == \"docs\"", "sphinx-lint; extra == \"docs\"", "sphinx-notfound-page<2,>=1; extra == \"docs\"", "sphinx-reredirects; extra == \"docs\"", + "sphinx<7.2.5; extra == \"docs\"", "sphinx>=3.5; extra == \"docs\"", "sphinxcontrib-towncrier; extra == \"docs\"", "tomli-w>=1.0.0; extra == \"testing\"", @@ -3503,7 +3639,7 @@ "wheel; extra == \"testing-integration\"" ], "requires_python": ">=3.8", - "version": "68.2.2" + "version": "69.0.2" }, { "artifacts": [ @@ -3527,28 +3663,41 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d", - "url": "https://files.pythonhosted.org/packages/10/68/09f08f931a8f4277a25feab191bbdaa443a6275184b3842aaae4bb486392/SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384", + "url": "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9", - "url": "https://files.pythonhosted.org/packages/27/7c/ab28273996e8e5b78ddaeddbc1df54033231ff325827b3149d51334ed852/SQLAlchemy-1.4.49.tar.gz" + "hash": "e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "url": "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz" + } + ], + "project_name": "sniffio", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "1.3.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace", + "url": "https://files.pythonhosted.org/packages/ea/d0/ba24be8ae3371efd477435e46d117431ca7c458e6c2a06fff3b2aa1c6b74/SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4", - "url": "https://files.pythonhosted.org/packages/47/3d/de827556bafdc40a4db10a5beccaee31d1840871ce867a372e7a37bf7c95/SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl" + "hash": "3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf", + "url": "https://files.pythonhosted.org/packages/5a/0a/dabe332c40afebb0a979d3e66b34570fce2f8611bae19b186f0c69f54643/SQLAlchemy-1.4.50.tar.gz" }, { "algorithm": "sha256", - "hash": "a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0", - "url": "https://files.pythonhosted.org/packages/82/d8/73bbde9576dc7d54bd3c619836347cd6bded6ea481f658f47e5aa41ceb8b/SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7", + "url": "https://files.pythonhosted.org/packages/82/60/9210ac87f2eecb047c291b9b415aad3d2a1931666b5489e5a2e474448e8c/SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" } ], "project_name": "sqlalchemy", "requires_dists": [ - "aiomysql; python_version >= \"3\" and extra == \"aiomysql\"", + "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\"", @@ -3580,7 +3729,7 @@ "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.49" + "version": "1.4.50" }, { "artifacts": [ @@ -3713,54 +3862,54 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17", - "url": "https://files.pythonhosted.org/packages/77/e7/3ad605fb700cfdca2b6c877713ca51239a5a11272e2340c79fc56849c5c4/tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl" + "hash": "71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f", + "url": "https://files.pythonhosted.org/packages/25/a3/1025f561b87b3cca6f66da149ba7ce4c2bb18d7bd6b84cd5a13a274e9dd3/tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f", - "url": "https://files.pythonhosted.org/packages/10/ed/deb0f6880e0ed0d13e68316a49ceb65817241d80e28fe54c61db16aeb7fa/tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579", + "url": "https://files.pythonhosted.org/packages/0e/76/aca8c8726d045c1c7b093cca3c5551e8df444ef74ba0dfd1f205da1f95db/tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a", - "url": "https://files.pythonhosted.org/packages/13/17/da173efad287dfe1f9dc93c9d6b2a5f9c4fed8ecb23966c9160014cfdd6e/tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl" + "hash": "27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263", + "url": "https://files.pythonhosted.org/packages/34/7a/e7ec972db24513ea69ac7132c1a5eef62309dc978566a4afffa314417a45/tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe", - "url": "https://files.pythonhosted.org/packages/48/64/679260ca0c3742e2236c693dc6c34fb8b153c14c21d2aa2077c5a01924d6/tornado-6.3.3.tar.gz" + "hash": "02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0", + "url": "https://files.pythonhosted.org/packages/4a/2e/3ba930e3af171847d610e07ae878e04fcb7e4d7822f1a2d29a27b128ea24/tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2", - "url": "https://files.pythonhosted.org/packages/66/a5/e6da56c03ff61200d5a43cfb75ab09316fc0836aa7ee26b4e9dcbfc3ae85/tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e", + "url": "https://files.pythonhosted.org/packages/62/e5/3ee2ba523a13bae5c17d1658580d13597116c1639374ca5033d58fd04724/tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a", - "url": "https://files.pythonhosted.org/packages/be/49/b60320323b7f5de3cd2fbd7717034eeb870cc5c7bfc641c85c0af9cfbc39/tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2", + "url": "https://files.pythonhosted.org/packages/66/e5/466aa544e0cbae9b0ece79cd42db257fa7bfa3197c853e3f7921b3963190/tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16", - "url": "https://files.pythonhosted.org/packages/d7/07/ffbdc4aa9f55eb006bb0a829b88fe264823df7d8fb9cce5f062720306c10/tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl" + "hash": "f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212", + "url": "https://files.pythonhosted.org/packages/9f/12/11d0a757bb67278d3380d41955ae98527d5ad18330b2edbdc8de222b569b/tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d", - "url": "https://files.pythonhosted.org/packages/e8/52/4775f3e6630bbc3808e678eb2294beeb654040cf45cc2b66cd6efdcf2571/tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl" + "hash": "72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee", + "url": "https://files.pythonhosted.org/packages/bd/a2/ea124343e3b8dd7712561fe56c4f92eda26865f5e1040b289203729186f2/tornado-6.4.tar.gz" }, { "algorithm": "sha256", - "hash": "ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0", - "url": "https://files.pythonhosted.org/packages/ec/85/c9e673e59931f793ef32ac8cd13f3f769b13c6ded2c14be9367020f947b7/tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl" + "hash": "88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78", + "url": "https://files.pythonhosted.org/packages/e2/40/bcf0af5a29a850bf5ad7f79ef51c054f99e18d9cdf4efd6eeb0df819641f/tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl" } ], "project_name": "tornado", "requires_dists": [], "requires_python": ">=3.8", - "version": "6.3.3" + "version": "6.4" }, { "artifacts": [ @@ -3814,19 +3963,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "98277f247f18b2c5cabaf4af369187754f4fb0e85911d473f72329db8a7f4fae", - "url": "https://files.pythonhosted.org/packages/85/e9/d82415708306eb348fb16988c4697076119dfbfa266f17f74e514a23a723/traitlets-5.11.2-py3-none-any.whl" + "hash": "f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33", + "url": "https://files.pythonhosted.org/packages/a7/1d/7d07e1b152b419a8a9c7f812eeefd408a0610d869489ee2e86973486713f/traitlets-5.14.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "7564b5bf8d38c40fa45498072bf4dc5e8346eb087bbf1e2ae2d8774f6a0f078e", - "url": "https://files.pythonhosted.org/packages/88/ec/5c4baa341ab8da0c7a9e70bf5bafe5aaeb0ff7c6f0cc84b2cf2a43b00cc6/traitlets-5.11.2.tar.gz" + "hash": "fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772", + "url": "https://files.pythonhosted.org/packages/25/a0/2feefaa884a7eaa83934476091ecfb2a3bc3b61c1ed98db3da0fbbf46e73/traitlets-5.14.0.tar.gz" } ], "project_name": "traitlets", "requires_dists": [ "argcomplete>=3.0.3; extra == \"test\"", - "mypy>=1.5.1; extra == \"test\"", + "mypy>=1.7.0; extra == \"test\"", "myst-parser; extra == \"docs\"", "pre-commit; extra == \"test\"", "pydata-sphinx-theme; extra == \"docs\"", @@ -3836,7 +3985,7 @@ "sphinx; extra == \"docs\"" ], "requires_python": ">=3.8", - "version": "5.11.2" + "version": "5.14.0" }, { "artifacts": [ @@ -3899,19 +4048,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "f7f8a25bfe306f2e6bc2ad0a2f949d9e72f2d91036d509c36d3810bf728bc6e1", - "url": "https://files.pythonhosted.org/packages/3a/04/cb753a05dfb30ed9e0eaa0fb447761d3399051eab1c618f5347f4339f364/types_cachetools-5.3.0.6-py3-none-any.whl" + "hash": "98c069dc7fc087b1b061703369c80751b0a0fc561f6fb072b554e5eee23773a0", + "url": "https://files.pythonhosted.org/packages/9a/2b/93146a80105a1ab180f15a5457d45706578ff0e20ba186d0d2ba7a75e3c3/types_cachetools-5.3.0.7-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "595f0342d246c8ba534f5a762cf4c2f60ecb61e8002b8b2277fd5cf791d4e851", - "url": "https://files.pythonhosted.org/packages/cf/85/78e40815bd412b39216edff562abb0c5614b71e395bae97f8332c6de661d/types-cachetools-5.3.0.6.tar.gz" + "hash": "27c982cdb9cf3fead8b0089ee6b895715ecc99dac90ec29e2cab56eb1aaf4199", + "url": "https://files.pythonhosted.org/packages/ef/bb/51a12d05e492d1648cd1cfc5b212f81fc8a8a5ea1610423574e7deea15ea/types-cachetools-5.3.0.7.tar.gz" } ], "project_name": "types-cachetools", "requires_dists": [], - "requires_python": null, - "version": "5.3.0.6" + "requires_python": ">=3.7", + "version": "5.3.0.7" }, { "artifacts": [ @@ -3955,21 +4104,21 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "19536aa3debfbe25a918cf0d898e9f5fbbe6f3594a429da7914bf331deb1b342", - "url": "https://files.pythonhosted.org/packages/6f/3e/802b4489dc34a5e6afae600cf0c8e0b58ae83aa6b5cd39fd6f8288ad33f1/types_pyOpenSSL-23.2.0.2-py3-none-any.whl" + "hash": "00171433653265843b7469ddb9f3c86d698668064cc33ef10537822156130ebf", + "url": "https://files.pythonhosted.org/packages/f5/b3/cd6c2344b922f116ba9d6b6e9f57f6d2f13a3514bd971b3e19a19383d28f/types_pyOpenSSL-23.3.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "6a010dac9ecd42b582d7dd2cc3e9e40486b79b3b64bb2fffba1474ff96af906d", - "url": "https://files.pythonhosted.org/packages/b1/0a/e38bd743f91fe6b54a2686dfe504acd1d883cc6fc5c9116bdb737a0f2622/types-pyOpenSSL-23.2.0.2.tar.gz" + "hash": "5ffb077fe70b699c88d5caab999ae80e192fe28bf6cda7989b7e79b1e4e2dcd3", + "url": "https://files.pythonhosted.org/packages/a4/43/32f874b5fa2240932d758c8f4350632e4edd19e662032ff4992ebcd8a882/types-pyOpenSSL-23.3.0.0.tar.gz" } ], "project_name": "types-pyopenssl", "requires_dists": [ "cryptography>=35.0.0" ], - "requires_python": null, - "version": "23.2.0.2" + "requires_python": ">=3.7", + "version": "23.3.0.0" }, { "artifacts": [ @@ -4011,13 +4160,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "05b1bf92879b25df20433fa1af07784a0d7928c616dc2ebf9087618db77ccbd0", - "url": "https://files.pythonhosted.org/packages/6d/71/abcbebae9f5bd0e41d39e7cf8709c4f06b8ab8334c21bf6a7cd8e0a69f05/types_redis-4.6.0.7-py3-none-any.whl" + "hash": "94fc61118601fb4f79206b33b9f4344acff7ca1d7bba67834987fb0efcf6a770", + "url": "https://files.pythonhosted.org/packages/5f/3e/63dabc15151422daad7b0f2f82c0a37d1edc5d9278dfc05fe7f9cc42cd3d/types_redis-4.6.0.11-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "28c4153ddb5c9d4f10def44a2454673c361d2d5fc3cd867cf3bb1520f3f59a38", - "url": "https://files.pythonhosted.org/packages/f7/64/5d64db485d6dc1bc6b3afdac5ecc0f50ed4f357b75bd73cc2ca9b53632f4/types-redis-4.6.0.7.tar.gz" + "hash": "c8cfc84635183deca2db4a528966c5566445fd3713983f0034fb0f5a09e0890d", + "url": "https://files.pythonhosted.org/packages/a9/b9/68d600512c73df22477f09236a978dbc00bd0f6215308c2deac69e6f9aa0/types-redis-4.6.0.11.tar.gz" } ], "project_name": "types-redis", @@ -4025,26 +4174,26 @@ "cryptography>=35.0.0", "types-pyOpenSSL" ], - "requires_python": null, - "version": "4.6.0.7" + "requires_python": ">=3.7", + "version": "4.6.0.11" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "77edcc843e53f8fc83bb1a840684841f3dc804ec94562623bfa2ea70d5a2ba1b", - "url": "https://files.pythonhosted.org/packages/5c/5a/fbfbe3d1db90c59fb0240cf13a84953677b15874d00e80e773425447633c/types_setuptools-68.2.0.0-py3-none-any.whl" + "hash": "8c86195bae2ad81e6dea900a570fe9d64a59dbce2b11cc63c046b03246ea77bf", + "url": "https://files.pythonhosted.org/packages/66/a3/9800e99c4081cb1bff0ec10bf6effb93edc9253ce2ec6db50be1a9d57053/types_setuptools-69.0.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "a4216f1e2ef29d089877b3af3ab2acf489eb869ccaf905125c69d2dc3932fd85", - "url": "https://files.pythonhosted.org/packages/1f/32/ad55729b96c07993bbf83a0a734a3ee8402ea42268939aeae30c4f3600d0/types-setuptools-68.2.0.0.tar.gz" + "hash": "b0a06219f628c6527b2f8ce770a4f47550e00d3e8c3ad83e2dc31bc6e6eda95d", + "url": "https://files.pythonhosted.org/packages/aa/dc/27d4819c27b504bbd2e8ae5aa907fe72c70af8ff90b8b4cdb96316275844/types-setuptools-69.0.0.0.tar.gz" } ], "project_name": "types-setuptools", "requires_dists": [], - "requires_python": null, - "version": "68.2.0.0" + "requires_python": ">=3.7", + "version": "69.0.0.0" }, { "artifacts": [ @@ -4219,13 +4368,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704", - "url": "https://files.pythonhosted.org/packages/58/19/a9ce39f89cf58cf1e7ce01c8bb76ab7e2c7aadbc5a2136c3e192097344f5/wcwidth-0.2.8-py2.py3-none-any.whl" + "hash": "f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c", + "url": "https://files.pythonhosted.org/packages/31/b1/a59de0ad3aabb17523a39804f4c6df3ae87ead053a4e25362ae03d73d03a/wcwidth-0.2.12-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4", - "url": "https://files.pythonhosted.org/packages/cb/ee/20850e9f388d8b52b481726d41234f67bc89a85eeade6e2d6e2965be04ba/wcwidth-0.2.8.tar.gz" + "hash": "f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02", + "url": "https://files.pythonhosted.org/packages/d7/12/63deef355537f290d5282a67bb7bdd165266e4eca93cd556707a325e5a24/wcwidth-0.2.12.tar.gz" } ], "project_name": "wcwidth", @@ -4233,7 +4382,7 @@ "backports.functools-lru-cache>=1.2.1; python_version < \"3.2\"" ], "requires_python": null, - "version": "0.2.8" + "version": "0.2.12" }, { "artifacts": [ @@ -4430,8 +4579,10 @@ "python-dotenv~=0.20.0", "python-json-logger>=2.0.1", "pyzmq~=24.0.1", + "raftify==0.0.62", "redis[hiredis]==4.5.5", "rich~=12.2", + "rraft-py==0.2.27", "setproctitle~=1.3.2", "tabulate~=0.8.9", "tblib~=1.7", diff --git a/requirements.txt b/requirements.txt index 67bd4a2c9ff..e2bdf361ae5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,3 +91,6 @@ types-tabulate backend.ai-krunner-alpine==5.1.0 backend.ai-krunner-static-gnu==4.1.0 + +rraft-py==0.2.27 +raftify==0.0.62 diff --git a/src/ai/backend/common/distributed.py b/src/ai/backend/common/distributed.py index b52fe2414a1..2a3563cc8b9 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 .logging import BraceStyleAdapter @@ -16,7 +18,77 @@ log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore[name-defined] -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 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..3616517f1d0 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 RaftFacade, RaftNode if TYPE_CHECKING: from ai.backend.common.bgtask import BackgroundTaskManager @@ -26,6 +27,31 @@ class BaseContext: pass +class RaftClusterContext: + _cluster: Optional[RaftFacade] = None + bootstrap_done: bool + node_id_start: int + + def __init__(self, bootstrap_done: bool = False, node_id_start: int = 1) -> None: + self.bootstrap_done = bootstrap_done + self.node_id_start = node_id_start + + def use_raft(self) -> bool: + return self._cluster is not None + + @property + def cluster(self) -> RaftFacade: + return cast(RaftFacade, self._cluster) + + @cluster.setter + def cluster(self, rhs: RaftFacade) -> None: + self._cluster = rhs + + @property + def raft_node(self) -> RaftNode: + return cast(RaftNode, cast(RaftFacade, self._cluster).raft_node) + + @attrs.define(slots=True, auto_attribs=True, init=False) class RootContext(BaseContext): pidx: int @@ -53,3 +79,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 48854d17f67..3d014c70289 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.logging import BraceStyleAdapter from ai.backend.common.types import AgentId, LogSeverity @@ -238,7 +242,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] @@ -250,14 +254,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 4a3141823d7..99107fc30ac 100644 --- a/src/ai/backend/manager/cli/__main__.py +++ b/src/ai/backend/manager/cli/__main__.py @@ -1,8 +1,10 @@ from __future__ import annotations import asyncio +import json import logging import pathlib +import pickle import subprocess import sys from datetime import datetime @@ -10,7 +12,11 @@ import click from more_itertools import chunked +from raftify.peers import Peer, Peers +from raftify.raft_client import RaftClient +from raftify.utils import SocketAddr from setproctitle import setproctitle +from tabulate import tabulate from ai.backend.cli.types import ExitCode from ai.backend.common import redis_helper as redis_helper @@ -325,6 +331,66 @@ async def _clear_terminated_sessions(): asyncio.run(_clear_terminated_sessions()) +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: + initial_peers = Peers( + { + int(peer_config["node-id"]): Peer( + addr=SocketAddr(peer_config["host"], peer_config["port"]) + ) + for peer_config in raft_configs.pop("peers") + } + ) + + peers: Peers | None = None + for peer in initial_peers.values(): + raft_client = RaftClient(peer.addr) + try: + resp = await raft_client.get_peers() + peers = pickle.loads(resp.peers) + except Exception: + continue + + if not peers: + print("No peers are available!") + return + + for peer in peers.values(): + raft_client = RaftClient(peer.addr) + + resp = await raft_client.debug_node() + + node_info = json.loads(resp.result) + is_leader = node_info["node_id"] == node_info["current_leader_id"] + table.append( + [ + peer.addr, + node_info["node_id"], + is_leader, + node_info["raft"]["term"], + node_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)) + + @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/config.py b/src/ai/backend/manager/config.py index e8a54342cb5..de3f660056f 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 ---------------------------- @@ -171,8 +173,6 @@ - {instance-id}: 1 # just a membership set """ -from __future__ import annotations - import json import logging import os @@ -215,6 +215,7 @@ SlotTypes, current_resource_slots, ) +from ai.backend.manager.types import RaftLogLovel, RaftSlogLovel from ..manager.defs import INTRINSIC_SLOTS from .api import ManagerStatus @@ -292,6 +293,44 @@ t.Key("aiomonitor-webui-port", default=49100): t.ToInt[1:65535], } ).allow_extra("*"), + t.Key("raft", default=None): ( + t.Dict( + { + t.Key("peers"): t.List( + t.Dict( + { + t.Key("node-id"): t.Int, + t.Key("host"): t.String, + t.Key("port"): t.Int, + } + ) + ), + t.Key("cluster-leader-id", default=1): t.Int, + 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.Bool | t.Null, + t.Key("batch-append", default=None): t.Bool | t.Null, + t.Key("max-uncommitted-size", default=None): t.Int | t.Null, + t.Key("skip-bcast-commit", default=None): t.Bool | t.Null, + t.Key("pre-vote", default=None): t.Bool | t.Null, + t.Key("priority", default=None): t.Int | t.Null, + t.Key("log-dir"): t.String, + t.Key("slog-level", default=None): tx.Enum(RaftSlogLovel) | t.Null, + t.Key("log-level", default=None): tx.Enum(RaftLogLovel) | t.Null, + } + ).allow_extra("*") + | t.Null + ), + t.Key("pipeline", default=None): t.Null | t.Dict( + { + t.Key("event-queue", default=None): t.Null | tx.HostPortPair, + }, + ).allow_extra("*"), t.Key("docker-registry"): t.Dict( { # deprecated in v20.09 t.Key("ssl-verify", default=True): t.ToBool, @@ -351,6 +390,7 @@ "threshold": {}, }, }, + "raft": None, } container_registry_iv = t.Dict( @@ -482,7 +522,6 @@ def container_registry_serialize(v: dict[str, Any]) -> dict[str, str]: ): session_hang_tolerance_iv, }, ).allow_extra("*"), - t.Key("roundrobin_states", default=None): t.Null | tx.RoundRobinStatesJSONString, } ).allow_extra("*") diff --git a/src/ai/backend/manager/idle.py b/src/ai/backend/manager/idle.py index 3243c7ac210..66b8c4a5b2a 100644 --- a/src/ai/backend/manager/idle.py +++ b/src/ai/backend/manager/idle.py @@ -35,7 +35,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, @@ -58,13 +62,14 @@ SessionTypes, ) from ai.backend.common.utils import nmget +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 @@ -169,6 +174,7 @@ class RemainingTimeType(str, enum.Enum): class IdleCheckerHost: + timer: AbstractGlobalTimer check_interval: ClassVar[float] = DEFAULT_CHECK_INTERVAL def __init__( @@ -177,6 +183,7 @@ def __init__( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, + raft_ctx: RaftClusterContext, lock_factory: DistributedLockFactory, ) -> None: self._checkers: list[BaseIdleChecker] = [] @@ -199,6 +206,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: @@ -218,13 +226,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, @@ -858,7 +875,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 = await redis_helper.execute( @@ -1060,6 +1077,7 @@ async def init_idle_checkers( shared_config: SharedConfig, event_dispatcher: EventDispatcher, event_producer: EventProducer, + raft_ctx: RaftClusterContext, lock_factory: DistributedLockFactory, ) -> IdleCheckerHost: """ @@ -1071,6 +1089,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..f4a5581f6ca --- /dev/null +++ b/src/ai/backend/manager/raft/__init__.py @@ -0,0 +1,19 @@ +from raftify.cli import AbstractCLIContext + +# Usage: +# raftify-cli bootstrap-cluster --module-name='ai.backend.manager.raft' + + +# TODO: Implement this. Maybe ProcessExecutor should be used for accomplishing this. +class RaftifyCLIContext(AbstractCLIContext): + async def bootstrap_cluster(self, args, options): + pass + + async def bootstrap_follower(self, args, options): + pass + + async def add_member(self, args, options): + pass + + async def remove_member(self, args, options): + pass diff --git a/src/ai/backend/manager/raft/logger.py b/src/ai/backend/manager/raft/logger.py new file mode 100644 index 00000000000..ff5c78b5ccd --- /dev/null +++ b/src/ai/backend/manager/raft/logger.py @@ -0,0 +1,71 @@ +import logging +import os +from typing import Optional + +from raftify import AbstractRaftifyLogger +from rraft import Logger + +from ai.backend.manager.types import RaftLogLovel, RaftSlogLovel + + +# Temporary logger for integration testing. +def setup_slog(level: Optional[RaftSlogLovel], log_path: str): + # Set up rraft-py's slog log-level to Debug. + # TODO: This method should be improved in rraft-py. + if level: + os.environ["RUST_LOG"] = level + return Logger.new_file_logger( + log_path, chan_size=1024, rotate_size=1024 * 1024 * 1024, rotate_keep=3 + ) + + +class RaftifyLogger(AbstractRaftifyLogger): + _log: Optional[logging.Logger] = None + + def __init__(self, log_level: Optional[RaftLogLovel]): + if log_level: + self._log = logging.getLogger("ai.backend.manager.scheduler") + self.log_level = log_level + + def _should_log(self, level: RaftLogLovel) -> bool: + if not self._log: + return False + + level_order = [ + RaftLogLovel.TRACE, + RaftLogLovel.DEBUG, + RaftLogLovel.INFO, + RaftLogLovel.ERROR, + RaftLogLovel.CRIT, + ] + return level_order.index(self.log_level) <= level_order.index(level) + + def info(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.INFO): + assert self._log is not None + self._log.info(message, *args, **kwargs) + + def debug(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.DEBUG): + assert self._log is not None + self._log.debug(message, *args, **kwargs) + + def warning(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.INFO): + assert self._log is not None + self._log.warning(message, *args, **kwargs) + + def error(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.ERROR): + assert self._log is not None + self._log.error(message, *args, **kwargs) + + def critical(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.CRIT): + assert self._log is not None + self._log.critical(message, *args, **kwargs) + + def verbose(self, message: str, *args, **kwargs): + if self._should_log(RaftLogLovel.VERBOSE): + assert self._log is not None + self._log.debug(message, *args, **kwargs) diff --git a/src/ai/backend/manager/raft/utils.py b/src/ai/backend/manager/raft/utils.py new file mode 100644 index 00000000000..1a435d36fab --- /dev/null +++ b/src/ai/backend/manager/raft/utils.py @@ -0,0 +1,114 @@ +import json +import pickle +from typing import Any, cast + +from aiohttp import web +from aiohttp.web import RouteTableDef +from raftify.log_entry.set_command import SetCommand +from raftify.raft_facade import RaftFacade +from raftify.state_machine.hashstore import HashStore + +routes = RouteTableDef() +""" +APIs of the web servers to interact with the RaftServers. +""" + + +@routes.get("/get/{id}") +async def get(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + store = cast(HashStore, raft_facade.store) + id = request.match_info["id"] + return web.Response(text=store.get(id)) + + +@routes.get("/all") +async def all(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + store = cast(HashStore, raft_facade.store) + return web.Response(text=json.dumps(store.as_dict())) + + +@routes.get("/put/{id}/{value}") +async def put(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + id, value = request.match_info["id"], request.match_info["value"] + message = SetCommand(id, value) + + result = await raft_facade.mailbox.send_proposal(message.encode()) + return web.Response(text=f'"{str(pickle.loads(result))}"') + + +@routes.get("/leave") +async def leave(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + id = raft_facade.raft_node.get_id() + addr = raft_facade.peers[id].addr + await raft_facade.mailbox.leave(id, addr) + return web.Response(text=f'Removed "node {id}" from the cluster successfully.') + + +@routes.get("/remove/{id}") +async def remove(request: web.Request) -> web.Response: + cluster: RaftFacade = request.app["state"]["raft"] + id = int(request.match_info["id"]) + addr = cluster.peers[id].addr + await cluster.mailbox.leave(id, addr) + return web.Response(text=f'Removed "node {id}" from the cluster successfully.') + + +@routes.get("/peers") +async def peers(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + return web.Response(text=str(raft_facade.peers)) + + +@routes.get("/leader") +async def leader(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + return web.Response(text=str(raft_facade.raft_node.get_leader_id())) + + +@routes.get("/transfer/{id}") +async def transfer_leader(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + target_node_id = int(request.match_info["id"]) + + raft_facade.raft_node.transfer_leader(target_node_id) + return web.Response(text="Leader transferred successfully.") + + +@routes.get("/snapshot") +async def snapshot(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + + await raft_facade.create_snapshot() + return web.Response(text="Created snapshot successfully.") + + +@routes.get("/unstable") +async def unstable(request: web.Request) -> web.Response: + raft_facade: RaftFacade = request.app["state"]["raft"] + return web.Response( + text=str(raft_facade.raft_node.raw_node.get_raft().get_raft_log().unstable()) + ) + + +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() diff --git a/src/ai/backend/manager/scheduler/dispatcher.py b/src/ai/backend/manager/scheduler/dispatcher.py index ada81692dbf..4d520a7f3e3 100644 --- a/src/ai/backend/manager/scheduler/dispatcher.py +++ b/src/ai/backend/manager/scheduler/dispatcher.py @@ -33,7 +33,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, @@ -61,15 +65,16 @@ SessionId, aobject, ) +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 from ..api.exceptions import GenericBadRequest, InstanceNotAvailable, SessionNotFound -from ..defs import SERVICE_MAX_RETRIES, LockID from ..exceptions import convert_to_status_data from ..models import ( - AgentRow, AgentStatus, EndpointLifecycle, EndpointRow, @@ -148,21 +153,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, @@ -170,6 +176,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 @@ -200,32 +207,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: @@ -233,7 +262,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( @@ -279,52 +307,33 @@ def _pipeline(r: Redis) -> RedisPipeline: ) try: - # The schedule() method should be executed with a global lock - # as its individual steps are composed of many short-lived transactions. - async with self.lock_factory(LockID.LOCKID_SCHEDULE, 60): - async with self.db.begin_readonly_session() as db_sess: - # query = ( - # sa.select(ScalingGroupRow) - # .join(ScalingGroupRow.agents.and_(AgentRow.status == AgentStatus.ALIVE)) - # ) - query = ( - sa.select(AgentRow.scaling_group) - .where(AgentRow.status == AgentStatus.ALIVE) - .group_by(AgentRow.scaling_group) - ) - 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, - ) - await redis_helper.execute( - self.redis_live, - lambda r: r.hset( - redis_key, - "resource_group", - 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)) - await redis_helper.execute( - self.redis_live, - lambda r: r.hset( - redis_key, - "finish_time", - datetime.now(tzutc()).isoformat(), - ), + async with self.db.begin_readonly_session() as db_sess: + # query = ( + # sa.select(ScalingGroupRow) + # .join(ScalingGroupRow.agents.and_(AgentRow.status == AgentStatus.ALIVE)) + # ) + query = ( + sa.select(AgentRow.scaling_group) + .where(AgentRow.status == AgentStatus.ALIVE) + .group_by(AgentRow.scaling_group) ) + 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( @@ -713,6 +722,7 @@ async def _schedule_single_node_session( log_fmt = _log_fmt.get("") log_args = _log_args.get(tuple()) requested_architectures = set(k.architecture for k in sess_ctx.kernels) + if len(requested_architectures) > 1: raise GenericBadRequest( "Cannot assign multiple kernels with different architectures' single node session", @@ -1233,91 +1243,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 cf8e1d504ec..b8920c2265b 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -18,9 +18,9 @@ AsyncIterator, Final, Iterable, - List, Mapping, MutableMapping, + Optional, Sequence, cast, ) @@ -30,6 +30,15 @@ import aiotools import click from aiohttp import web +from aiotools import process_index +from raftify.config import RaftifyConfig +from raftify.peers import Peer, Peers +from raftify.raft_client import RaftClient +from raftify.raft_facade import RaftFacade +from raftify.raft_utils import RequestIdResponse +from raftify.rraft_deserializer import init_rraft_py_deserializer +from raftify.state_machine.hashstore import HashStore +from raftify.utils import SocketAddr from setproctitle import setproctitle from ai.backend.common import redis_helper @@ -49,11 +58,13 @@ from ai.backend.common.plugin.monitor import INCREMENT from ai.backend.common.types import AgentSelectionStrategy, LogSeverity from ai.backend.common.utils import env_info +from ai.backend.manager.raft.logger import RaftifyLogger, setup_slog +from ai.backend.manager.raft.utils import WebServer 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, @@ -413,6 +424,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() @@ -490,6 +502,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, @@ -636,6 +649,98 @@ async def _force_terminate_hanging_sessions( await task +def grant_type(value: Optional[str]) -> Any: + if value is None: + return None + try: + return int(value) + except Exception: + pass + if value.lower() == "true": + return True + if value.lower() == "false": + return False + return value + + +@actxmgr +async def raft_ctx(root_ctx: RootContext) -> AsyncIterator[None]: + local_config = root_ctx.local_config + init_rraft_py_deserializer() + + raft_configs = local_config.get("raft") + + if raft_configs is not None: + initial_peers = Peers( + { + int(peer_config["node-id"]): Peer( + addr=SocketAddr(peer_config["host"], peer_config["port"]) + ) + for peer_config in raft_configs.pop("peers") + } + ) + + node_id_start = root_ctx.raft_ctx.node_id_start + + raft_configs_dict = { + key.replace("-", "_"): grant_type(raft_configs[key]) for key in raft_configs + } + + logger = RaftifyLogger(raft_configs_dict.get("log_level")) + + raft_cfg = RaftifyConfig( + auto_remove_node=False, + log_dir=raft_configs_dict["log_dir"], + compacted_log_dir=raft_configs_dict["log_dir"], + raft_config=RaftifyConfig.new_raft_config(raft_configs_dict), + ) + + store = HashStore() + + logger.info("Bootstrapping Raft cluster...") + node_id = node_id_start + process_index.get() + current_node = [peer for id, peer in initial_peers.data.items() if id == node_id][0] + raft_addr = current_node.addr + + slog = setup_slog( + raft_configs_dict.get("slog_level"), + f"{raft_configs_dict['log_dir']}/slog-{node_id}.log", + ) + + root_ctx.raft_ctx.cluster = RaftFacade( + raft_cfg, raft_addr, store, slog, logger, initial_peers=initial_peers + ) + raft_cluster = root_ctx.raft_ctx.cluster + raft_cluster.run_raft(node_id) + + if node_id == 1: + assert not root_ctx.raft_ctx.bootstrap_done + asyncio.create_task(raft_cluster.wait_for_followers_join()) + else: + # Wait for the leader node's grpc server ready + await asyncio.sleep(2) + leader_id = raft_configs["cluster-leader-id"] + leader_client = RaftClient(initial_peers[leader_id].addr) + + if root_ctx.raft_ctx.bootstrap_done: + await raft_cluster.join_cluster( + RequestIdResponse( + follower_id=node_id, + leader=(leader_id, leader_client), + peers=initial_peers, + ) + ) + else: + await raft_cluster.send_member_bootstrap_ready_msg(node_id) + + asyncio.create_task(raft_cluster.wait_for_termination()) + # Only for testing + asyncio.create_task(WebServer(f"127.0.0.1:6025{node_id}", {"raft": raft_cluster}).run()) + + assert root_ctx.raft_ctx.cluster.raft_node is not None, "RaftNode not initialized properly!" + yield + + class background_task_ctx: def __init__(self, root_ctx: RootContext) -> None: self.root_ctx = root_ctx @@ -781,6 +886,7 @@ def build_root_app( database_ctx, distributed_lock_ctx, event_dispatcher_ctx, + raft_ctx, idle_checker_ctx, storage_manager_ctx, hook_plugin_ctx, @@ -834,11 +940,15 @@ async def _call_cleanup_context_shutdown_handlers(app: web.Application) -> None: async def server_main( loop: asyncio.AbstractEventLoop, pidx: int, - _args: List[Any], + _args: list[Any], ) -> AsyncIterator[None]: root_app = build_root_app(pidx, _args[0], subapp_pkgs=global_subapp_pkgs) root_ctx: RootContext = root_app["_root.context"] + bootstrap_done = _args[2] + node_id_start = _args[3] + root_ctx.raft_ctx = RaftClusterContext(bootstrap_done, node_id_start) + # Start aiomonitor. # Port is set by config (default=50100 + pidx). loop.set_debug(root_ctx.local_config["debug"]["asyncio"]) @@ -913,9 +1023,10 @@ async def server_main( async def server_main_logwrapper( loop: asyncio.AbstractEventLoop, pidx: int, - _args: List[Any], + _args: list[Any], ) -> AsyncIterator[None]: setproctitle(f"backend.ai: manager worker-{pidx}") + log_endpoint = _args[1] logger = Logger(_args[0]["logging"], is_master=False, log_endpoint=log_endpoint) try: @@ -946,13 +1057,40 @@ async def server_main_logwrapper( default="INFO", help="Set the logging verbosity level", ) +@click.option( + "--bootstrap-done", + type=bool, + is_flag=True, + default=False, + help=( + "This would be useful when adding the " + "RaftNode to an existing cluster without restarting the server." + ), +) +# TODO: Find better way to implement this. +@click.option( + "--node-id-start", + type=int, + default=1, + help="Set this to the max(node_ids) + 1 when joining a RaftNode to another cluster.", +) @click.pass_context -def main(ctx: click.Context, config_path: Path, log_level: str, debug: bool = False) -> None: +def main( + ctx: click.Context, + config_path: Path, + log_level: str, + debug: bool = False, + bootstrap_done: bool = False, + node_id_start: int = 1, +) -> None: """ Start the manager service as a foreground process. """ cfg = load_config(config_path, "DEBUG" if debug else log_level) + if bootstrap_done and node_id_start == 1: + raise click.UsageError("--node-id-start must be provided when --bootstrap-done is True.") + if ctx.invoked_subcommand is None: cfg["manager"]["pid-file"].write_text(str(os.getpid())) ipc_base_path = cfg["manager"]["ipc-base-path"] @@ -976,7 +1114,7 @@ def main(ctx: click.Context, config_path: Path, log_level: str, debug: bool = Fa aiotools.start_server( server_main_logwrapper, num_workers=cfg["manager"]["num-proc"], - args=(cfg, log_endpoint), + args=(cfg, log_endpoint, bootstrap_done, node_id_start), wait_timeout=5.0, ) finally: diff --git a/src/ai/backend/manager/types.py b/src/ai/backend/manager/types.py index 7d413594deb..914c4579c96 100644 --- a/src/ai/backend/manager/types.py +++ b/src/ai/backend/manager/types.py @@ -41,3 +41,20 @@ class UserScope: class DistributedLockFactory(Protocol): def __call__(self, lock_id: LockID, lifetime_hint: float) -> AbstractDistributedLock: ... + + +class RaftSlogLovel(str, enum.Enum): + INFO = "info" + TRACE = "trace" + DEBUG = "debug" + CRIT = "crit" + ERROR = "error" + + +class RaftLogLovel(str, enum.Enum): + INFO = "info" + TRACE = "trace" + DEBUG = "debug" + CRIT = "crit" + ERROR = "error" + VERBOSE = "verbose" diff --git a/tests/common/test_distributed.py b/tests/common/test_distributed.py index c4cc6e558d6..59cc56fa0d7 100644 --- a/tests/common/test_distributed.py +++ b/tests/common/test_distributed.py @@ -18,7 +18,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.events import AbstractEvent, EventDispatcher, EventProducer from ai.backend.common.lock import AbstractDistributedLock, EtcdLock, FileLock, RedisLock @@ -98,7 +98,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), @@ -150,7 +150,7 @@ async def _tick(context: Any, source: AgentId, event: NoopEvent) -> None: ConfigScopes.NODE: "node/i-test", }, ) - timer = GlobalTimer( + timer = DistributedLockGlobalTimer( EtcdLock(etcd_ctx.lock_name, etcd, timeout=None, debug=True), event_producer, lambda: NoopEvent(timer_ctx.test_case_ns), @@ -207,7 +207,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), @@ -398,7 +398,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 2174ebd981c..7f9eb214e67 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( @@ -846,6 +862,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( @@ -928,6 +945,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 2bd8e4c4f68..d020cc8a774 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 @@ -1169,6 +1170,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, @@ -1176,6 +1178,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, )