diff --git a/Makefile b/Makefile index a8c4d9846e..45043bc352 100644 --- a/Makefile +++ b/Makefile @@ -184,12 +184,6 @@ install-runners: @echo "" # NOTE: We use xargs to speed things up by installing runners in parallel echo -e "$(COMPONENTS_RUNNERS)" | tr -d "\n" | xargs -P $(XARGS_CONCURRENCY) -d " " -n1 -i bash -c "set -x; . $(VIRTUALENV_DIR)/bin/activate; cd $$(pwd)/{} ; $(PYBIN) -m pip install --editable . --no-deps" - #@for component in $(COMPONENTS_RUNNERS); do \ - # echo "==========================================================="; \ - # echo "Installing runner:" $$component; \ - # echo "==========================================================="; \ - # #(. $(VIRTUALENV_DIR)/bin/activate; cd $$component; python -m pip install --editable . --no-deps); \ - #done .PHONY: install-mock-runners install-mock-runners: @@ -198,12 +192,6 @@ install-mock-runners: @echo "" # NOTE: We use xargs to speed things up by installing runners in parallel echo -e "$(MOCK_RUNNERS)" | tr -d "\n" | xargs -P $(XARGS_CONCURRENCY) -d " " -n1 -i sh -c ". $(VIRTUALENV_DIR)/bin/activate; cd $$(pwd)/{} ; $(PYBIN) -m pip install --editable . --no-deps" - #@for component in $(MOCK_RUNNERS); do \ - # echo "==========================================================="; \ - # echo "Installing mock runner:" $$component; \ - # echo "==========================================================="; \ - # (. $(VIRTUALENV_DIR)/bin/activate; cd $$component; python -m pip install --editable . --no-deps); \ - #done .PHONY: check-requirements .check-requirements: @@ -267,7 +255,7 @@ check-python-packages: echo "==========================================================="; \ echo "Checking component:" $$component; \ echo "==========================================================="; \ - (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) setup.py --version) || exit 1; \ + (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) -m setuptools_scm) || exit 1; \ done .PHONY: check-python-packages-nightly @@ -284,8 +272,8 @@ check-python-packages-nightly: echo "==========================================================="; \ echo "Checking component:" $$component; \ echo "==========================================================="; \ - (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) setup.py --version) || exit 1; \ - (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) setup.py sdist bdist_wheel) || exit 1; \ + (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) -m setuptools_scm) || exit 1; \ + (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) -m build) || exit 1; \ (set -e; cd $$component; ../$(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) -m pip install --editable . --no-deps) || exit 1; \ ($(VIRTUALENV_COMPONENTS_DIR)/bin/$(PYBIN) -c "import $$component") || exit 1; \ (set -e; cd $$component; rm -rf dist/; rm -rf $$component.egg-info) || exit 1; \ @@ -662,13 +650,6 @@ distclean: clean scripts/write-headers.sh $$component/dist_utils.py || break;\ done - # Copy over CHANGELOG.RST, CONTRIBUTING.RST and LICENSE file to each component directory - #@for component in $(COMPONENTS_TEST); do\ - # test -s $$component/README.rst || cp -f README.rst $$component/; \ - # cp -f CONTRIBUTING.rst $$component/; \ - # cp -f LICENSE $$component/; \ - #done - .PHONY: .requirements .requirements: virtualenv $(VIRTUALENV_DIR)/bin/$(PYBIN) -m pip install --upgrade "pip==$(PIP_VERSION)" @@ -681,16 +662,10 @@ distclean: clean # Remove any *.egg-info files which polute PYTHONPATH rm -rf *.egg-info* - # Generate finall requirements.txt file for each component + # Generate final requirements.txt file for each component # NOTE: We use xargs to speed things up by running commands in parallel echo -e "$(COMPONENTS_WITH_RUNNERS)" | tr -d "\n" | xargs -P $(XARGS_CONCURRENCY) -d " " -n1 -i sh -c "$(VIRTUALENV_DIR)/bin/$(PYBIN) scripts/fixate-requirements.py --skip=virtualenv,virtualenv-osx -s {}/in-requirements.txt -f fixed-requirements.txt -o {}/requirements.txt" - #@for component in $(COMPONENTS_WITH_RUNNERS); do\ - # echo "==========================================================="; \ - # echo "Generating requirements.txt for" $$component; \ - # $(VIRTUALENV_DIR)/bin/python scripts/fixate-requirements.py --skip=virtualenv,virtualenv-osx -s $$component/in-requirements.txt -f fixed-requirements.txt -o $$component/requirements.txt; \ - #done - @echo "===========================================================" .PHONY: requirements @@ -781,21 +756,6 @@ endif echo 'export PYTHONPATH' >> $(VIRTUALENV_DIR)/bin/activate touch $(VIRTUALENV_DIR)/bin/activate - # Setup PYTHONPATH in fish activate script... - #echo '' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo 'set -gx _OLD_PYTHONPATH $$PYTHONPATH' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo 'set -gx PYTHONPATH $$_OLD_PYTHONPATH $(COMPONENT_PYTHONPATH)' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo 'functions -c deactivate old_deactivate' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo 'function deactivate' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' if test -n $$_OLD_PYTHONPATH' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' set -gx PYTHONPATH $$_OLD_PYTHONPATH' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' set -e _OLD_PYTHONPATH' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' end' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' old_deactivate' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo ' functions -e old_deactivate' >> $(VIRTUALENV_DIR)/bin/activate.fish - #echo 'end' >> $(VIRTUALENV_DIR)/bin/activate.fish - #touch $(VIRTUALENV_DIR)/bin/activate.fish - # debug pip installed packages $(VIRTUALENV_DIR)/bin/pip list @@ -867,7 +827,7 @@ endif @echo @echo "----- Dropping st2-test db -----" @mongosh st2-test --eval "db.dropDatabase();" - failed=0; \ + failed=(); \ for component in $(COMPONENTS_TEST); do\ echo "==========================================================="; \ echo "Running tests in" $$component; \ @@ -878,13 +838,13 @@ endif ST2TESTS_REDIS_PORT=$(ST2TESTS_REDIS_PORT) \ COVERAGE_FILE=.coverage.unit.$$(echo $$component | tr '/' '.') \ pytest --verbose $(PYTEST_OPTS) --cov=$$component --cov-branch \ - $$component/tests/unit || ((failed+=1)); \ + $$component/tests/unit || failed+=($$component); \ echo "-----------------------------------------------------------"; \ echo "Done running tests in" $$component; \ echo "==========================================================="; \ done; \ - echo pytest runs failed=$$failed; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi .PHONY: .combine-unit-tests-coverage .combine-unit-tests-coverage: .run-unit-tests-coverage @@ -929,7 +889,7 @@ itests: requirements .itests @echo @echo "----- Dropping st2-test db -----" @mongosh st2-test --eval "db.dropDatabase();" - @failed=0; \ + @failed=(); \ for component in $(COMPONENTS_TEST); do\ echo "==========================================================="; \ echo "Running integration tests in" $$component; \ @@ -937,13 +897,13 @@ itests: requirements .itests . $(VIRTUALENV_DIR)/bin/activate; \ EVENTLET_TESTS=1 \ pytest --capture=no --verbose $(PYTEST_OPTS) \ - $$component/tests/integration || ((failed+=1)); \ + $$component/tests/integration || failed+=($$component); \ echo "-----------------------------------------------------------"; \ echo "Done running integration tests in" $$component; \ echo "==========================================================="; \ done; \ - echo pytest runs failed=$$failed; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi .PHONY: .run-integration-tests-coverage ifdef INCLUDE_TESTS_IN_COVERAGE @@ -955,7 +915,7 @@ endif @echo @echo "----- Dropping st2-test db -----" @mongosh st2-test --eval "db.dropDatabase();" - @failed=0; \ + @failed=(); \ for component in $(COMPONENTS_TEST); do\ echo "==========================================================="; \ echo "Running integration tests in" $$component; \ @@ -964,13 +924,13 @@ endif EVENTLET_TESTS=1 \ COVERAGE_FILE=.coverage.integration.$$(echo $$component | tr '/' '.') \ pytest --capture=no --verbose $(PYTEST_OPTS) --cov=$$component --cov-branch \ - $$component/tests/integration || ((failed+=1)); \ + $$component/tests/integration || failed+=($$component); \ echo "-----------------------------------------------------------"; \ echo "Done integration running tests in" $$component; \ echo "==========================================================="; \ done; \ - echo pytest runs failed=$$failed; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi # NOTE: If you also want to run orquesta tests which seem to have a bunch of race conditions, use # ci-integration-full target # @echo @@ -1039,13 +999,6 @@ endif make .coverage-combine; \ fi -# @for coverage_result in $(COVERAGE_GLOBS); do \ -# [ -e $${coverage_result} ] || echo "$${coverage_result} does not exist." && continue; \ -# echo "Combining data from $${coverage_result}"; \ -# . $(VIRTUALENV_DIR)/bin/activate; coverage combine $${coverage_result}; \ -# done || \ -# (echo "Running .coverage-combine"; make .coverage-combine) - .PHONY: .coverage-report .coverage-report: .coverage . $(VIRTUALENV_DIR)/bin/activate; coverage report @@ -1096,14 +1049,16 @@ runners-tests: requirements .runners-tests @echo @echo "----- Dropping st2-test db -----" @mongosh st2-test --eval "db.dropDatabase();" - @failed=0; \ + @failed=(); \ for component in $(COMPONENTS_RUNNERS); do\ echo "==========================================================="; \ echo "Running tests in" $$component; \ echo "==========================================================="; \ - . $(VIRTUALENV_DIR)/bin/activate; pytest --capture=no --verbose $(PYTEST_OPTS) $$component/tests/unit || ((failed+=1)); \ + . $(VIRTUALENV_DIR)/bin/activate; \ + pytest --capture=no --verbose $(PYTEST_OPTS) $$component/tests/unit || failed+=($$component); \ done; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi .PHONY: runners-itests runners-itests: requirements .runners-itests @@ -1114,15 +1069,16 @@ runners-itests: requirements .runners-itests @echo "==================== runners-itests ====================" @echo @echo "----- Dropping st2-test db -----" - @failed=0; \ + @failed=(); \ for component in $(COMPONENTS_RUNNERS); do\ echo "==========================================================="; \ echo "Running integration tests in" $$component; \ echo "==========================================================="; \ - . $(VIRTUALENV_DIR)/bin/activate; pytest --capture=no --verbose $(PYTEST_OPTS) $$component/tests/integration || ((failed+=1)); \ + . $(VIRTUALENV_DIR)/bin/activate; \ + pytest --capture=no --verbose $(PYTEST_OPTS) $$component/tests/integration || failed+=($$component); \ done; \ - echo pytest runs failed=$$failed; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi .PHONY: .runners-itests-coverage-html .runners-itests-coverage-html: @@ -1130,16 +1086,16 @@ runners-itests: requirements .runners-itests @echo "============== runners-itests-coverage-html ==============" @echo @echo "The tests assume st2 is running on 127.0.0.1." - @failed=0; \ + @failed=(); \ for component in $(COMPONENTS_RUNNERS); do\ echo "==========================================================="; \ echo "Running integration tests in" $$component; \ echo "==========================================================="; \ . $(VIRTUALENV_DIR)/bin/activate; pytest --capture=no --verbose $(PYTEST_OPTS) \ - --cov=$$component --cov-report=html $$component/tests/integration || ((failed+=1)); \ + --cov=$$component --cov-report=html $$component/tests/integration || failed+=($$component); \ done; \ - echo pytest runs failed=$$failed; \ - if [ $$failed -gt 0 ]; then exit 1; fi + echo "pytest runs failed=$${failed[@]}"; \ + if [ $${#failed[@]} -gt 0 ]; then exit 1; fi .PHONY: cli cli: diff --git a/contrib/runners/action_chain_runner/action_chain_runner/action_chain_runner.py b/contrib/runners/action_chain_runner/action_chain_runner/action_chain_runner.py index 57c015adcf..82af625004 100644 --- a/contrib/runners/action_chain_runner/action_chain_runner/action_chain_runner.py +++ b/contrib/runners/action_chain_runner/action_chain_runner/action_chain_runner.py @@ -15,7 +15,6 @@ from __future__ import absolute_import -import eventlet import traceback import uuid import datetime @@ -24,6 +23,7 @@ from jsonschema import exceptions as json_schema_exc from oslo_config import cfg +from st2common.util import concurrency from st2common.runners.base import ActionRunner from st2common.runners.base import get_metadata as get_runner_metadata from st2common import log as logging @@ -879,7 +879,7 @@ def _run_action(self, liveaction, wait_for_completion=True, sleep_delay=1.0): action_constants.LIVEACTION_STATUS_PENDING, ] ): - eventlet.sleep(sleep_delay) + concurrency.sleep(sleep_delay) liveaction = action_db_util.get_liveaction_by_id(liveaction.id) return liveaction @@ -901,7 +901,7 @@ def _resume_action(self, liveaction, wait_for_completion=True, sleep_delay=1.0): action_constants.LIVEACTION_COMPLETED_STATES + [action_constants.LIVEACTION_STATUS_PAUSED] ): - eventlet.sleep(sleep_delay) + concurrency.sleep(sleep_delay) liveaction = action_db_util.get_liveaction_by_id(liveaction.id) return liveaction diff --git a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_cancel.py b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_cancel.py index bf6ffdd47a..6c4e3b8397 100644 --- a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_cancel.py +++ b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_cancel.py @@ -14,8 +14,9 @@ # limitations under the License. from __future__ import absolute_import -import eventlet import mock + +from st2common.util import concurrency import os import tempfile @@ -98,7 +99,7 @@ def _wait_for_children(self, execution, interval=0.1, retries=100): for i in range(0, retries): execution = ActionExecution.get_by_id(str(execution.id)) if len(getattr(execution, "children", [])) <= 0: - eventlet.sleep(interval) + concurrency.sleep(interval) continue return execution diff --git a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_notifications.py b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_notifications.py index f2f2a680c8..82cd10cbe6 100644 --- a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_notifications.py +++ b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_notifications.py @@ -14,9 +14,10 @@ # limitations under the License. from __future__ import absolute_import -import eventlet import mock +from st2common.util import concurrency + from st2common.bootstrap import actionsregistrar from st2common.bootstrap import runnersregistrar from st2common.constants import action as action_constants @@ -221,7 +222,7 @@ def _wait_for_children( % (expected_children, found_children) ) - eventlet.sleep(interval) + concurrency.sleep(interval) found_children = len(getattr(execution, "children", [])) diff --git a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_pause_resume.py b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_pause_resume.py index e0dfecc4d6..fa8a0c2245 100644 --- a/contrib/runners/action_chain_runner/tests/unit/test_actionchain_pause_resume.py +++ b/contrib/runners/action_chain_runner/tests/unit/test_actionchain_pause_resume.py @@ -14,8 +14,9 @@ # limitations under the License. from __future__ import absolute_import -import eventlet import mock + +from st2common.util import concurrency import os import tempfile @@ -120,7 +121,7 @@ def _wait_for_status(self, liveaction, status, interval=0.1, retries=100): for i in range(0, retries): liveaction = LiveAction.get_by_id(str(liveaction.id)) if liveaction.status != status: - eventlet.sleep(interval) + concurrency.sleep(interval) continue else: break @@ -132,7 +133,7 @@ def _wait_for_children(self, execution, interval=0.1, retries=100): for i in range(0, retries): execution = ActionExecution.get_by_id(str(execution.id)) if len(getattr(execution, "children", [])) <= 0: - eventlet.sleep(interval) + concurrency.sleep(interval) continue return execution diff --git a/fixed-requirements.txt b/fixed-requirements.txt index 81b1798e56..246ddce30f 100644 --- a/fixed-requirements.txt +++ b/fixed-requirements.txt @@ -11,16 +11,15 @@ apscheduler==3.11.0 chardet==5.2.0 cffi==1.17.1 cryptography==43.0.3 -# Gunicorn <26 requires eventlet>=0.40.3 -eventlet==0.40.3 +eventlet==0.40.4 # Note: installs gitpython==3.1.37 (security fixed) under py3.8 and gitpython==3.1.18 (latest available, vulnerable) under py3.6 gitpython==3.1.45 # Needed by gitpython, old versions used to bundle it gitdb==4.0.12 -# Note: greenlet is used by eventlet -greenlet==3.1.1 -# Gunicorn dropped eventlet support as of v26. https://gunicorn.org/news/#2600-2026-05-05 -gunicorn<26 +gevent==25.9.1 +# Note: greenlet is used by eventlet & gevent (3.2.2 is the minimum common version) +greenlet==3.2.2 +gunicorn==23.0.0 # importlib backport replaces setuptool's pkg_resource. importlib_resources==6.5.2 importlib_metadata==9.0.0 diff --git a/lockfiles/st2.lock b/lockfiles/st2.lock index 546f12698e..e76a34b7de 100644 --- a/lockfiles/st2.lock +++ b/lockfiles/st2.lock @@ -21,6 +21,7 @@ // "editor", // "eventlet>=0.40.3", // "flask", +// "gevent", // "gitdb", // "gitpython", // "graphviz", @@ -1527,13 +1528,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "bc22396093cb4119ff7007776be6a5348a613ccd42eeb0f9519853a6efcbcabe", - "url": "https://files.pythonhosted.org/packages/c3/1c/febe9acf1b4f0d67603b231c28d6d17d647d68c90c1963fecdeb64046d6d/eventlet-0.41.0-py3-none-any.whl" + "hash": "6f7bb5c2309d1c4527bf15fc2a5da0b829e68e495430b890993b47dfea258ae5", + "url": "https://files.pythonhosted.org/packages/a8/55/92d859c16a37a9b70e2c682747750184b389a6b4d25321a4a1ec48d94b33/eventlet-0.41.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "35df85f0ccd3e73effb6fd9f1ceae46b500b966c7da1817289c323a307bd397b", - "url": "https://files.pythonhosted.org/packages/d3/90/32772ae7c9897554c56b9367b67478a3dc89c70d9b4d12e241746f6fdae3/eventlet-0.41.0.tar.gz" + "hash": "e91010caa1880bb511de6ce2ed2186ef3493e0762a4d3ee93e97a0fcccdaaa28", + "url": "https://files.pythonhosted.org/packages/8f/03/9562f7aa854e001e3a63034c0a97590a1546e4fe530abf511c2ce07b0cb1/eventlet-0.41.1.tar.gz" } ], "project_name": "eventlet", @@ -1542,14 +1543,14 @@ "build; extra == \"dev\"", "commitizen; extra == \"dev\"", "dnspython>=1.15.0", - "greenlet>=1.0", + "greenlet<3.4", "isort; extra == \"dev\"", "pip-tools; extra == \"dev\"", "pre-commit; extra == \"dev\"", "twine; extra == \"dev\"" ], "requires_python": ">=3.10", - "version": "0.41.0" + "version": "0.41.1" }, { "artifacts": [ @@ -1596,19 +1597,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", - "url": "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl" + "hash": "06e1c6d5e12277022172b7f8d090affe0e8f940f1a37ca45f548855ee51a6b86", + "url": "https://files.pythonhosted.org/packages/e7/20/b0d6da41a08b30faef4f20302f5d826a9d39073750cd9100b463eb840cf4/filelock-3.30.3-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", - "url": "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz" + "hash": "6575420cd497ed15fc43a7ac46c48f6b6371c733fc40cf5a4e216871397bc1e2", + "url": "https://files.pythonhosted.org/packages/d6/01/9256cbe36eb2a84501920adad7d0e86738ab0ad60e64f4f5c38d7dd94dc1/filelock-3.30.3.tar.gz" } ], "project_name": "filelock", "requires_dists": [], "requires_python": ">=3.10", - "version": "3.29.7" + "version": "3.30.3" }, { "artifacts": [ @@ -1658,6 +1659,118 @@ "requires_python": ">=3.10", "version": "3.3.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "0bf57df54f1c66273bf3601c2a1e41b12138fe848933718369663bc54f177ca2", + "url": "https://files.pythonhosted.org/packages/f9/3c/2fe77ee6e3d381b3c50c0b7d6c4c08c08b8ff5e8c0d9dd51a3b426d61eec/gevent-26.5.0-cp312-cp312-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "cd3dc60581687e2618286108f8e2f820d8446be4b34131065011c066e911d39c", + "url": "https://files.pythonhosted.org/packages/2a/ea/ea87c08931c9e4c6c40bb05a2cb19c2d6f93fe6e0052f9152ea5ade6d037/gevent-26.5.0-cp311-cp311-manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "e80ad2a8a1e8bdaa5605e3bf4929e0cebf9ea7b8237c83362f7257698bb14280", + "url": "https://files.pythonhosted.org/packages/37/0b/1a530b2db55c97cc0cf44116201f538f3033c04c1d2aca143979b412f4be/gevent-26.5.0-cp312-cp312-macosx_11_0_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "68c5fc21cef80268cdff88a4ae6c025fabb019b071f6f8ee4d20a7bccbddb873", + "url": "https://files.pythonhosted.org/packages/3a/4c/7f5ed67e52dfdef4ff91ae1a6fb28186d52e2496962edc8f17bdea9ab2c0/gevent-26.5.0-cp311-cp311-manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "d325502eb0695708ef8c899f605573ed6847f3961f8159627dba267fbf3ce457", + "url": "https://files.pythonhosted.org/packages/4c/75/0f5da6ca045f8a052203e1810058029f4b682507a789b413cac7d28bae28/gevent-26.5.0-cp311-cp311-manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "bccff69c462e3650a0fd1d4e9cfc8b6effe15f3e9b1cad20a7bb5ce14b057efd", + "url": "https://files.pythonhosted.org/packages/4e/69/1559b1f6b5107a9118fccd300240879bd581b6d87b03d568d0d155ea702c/gevent-26.5.0-cp312-cp312-manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "a11daf3a588b932c8bf965fb18444c69aff48badec88435e988cf8d67137075a", + "url": "https://files.pythonhosted.org/packages/6e/f1/fcff7f7fad2bb33f3742db6b2145825a2191c0cd31d75789b0741fd28faf/gevent-26.5.0-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "2ba673dcbf7747513b58fa64ca7e9d6a828bc5c604d1552d23db89006d7911df", + "url": "https://files.pythonhosted.org/packages/72/b7/01a5880e01702f39fb09e3616c624054a0dc9a82561a865f3b1eff4bfc80/gevent-26.5.0-cp310-cp310-manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "271b1474d81bb33036631adb16a35e5a1ee9dc414b05c999d6b01dc839a89975", + "url": "https://files.pythonhosted.org/packages/8d/fe/035ec5fa58a886740a744380118f03a90ac2da3f6c9cba248f28074ce40a/gevent-26.5.0-cp310-cp310-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "1101b5ef82a3fb178550cfd80f32293dc8dd2f3d0828292223ebba29d6f76e33", + "url": "https://files.pythonhosted.org/packages/98/57/151314f00bdc6ba77333febb3e9dc97fdf94d79426559b4fa8332f0c2b6e/gevent-26.5.0-cp311-cp311-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "96d5e96b1b14a4c1023dcfcc114533217f13febc3b6169254f23fc18d19fee29", + "url": "https://files.pythonhosted.org/packages/9f/b1/5f8a4196113cf7f3fdd987b483f7e6b10c28ea3930c4727e31ba8cce51b6/gevent-26.5.0-cp312-cp312-manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "fe42c037253580a3386fce275f8a2a845e540f5a729916934a732f13d42e72cc", + "url": "https://files.pythonhosted.org/packages/b9/df/32fe851ed5f68493f354e09b19bdebae0de1185be4db0b2988e71e737fd3/gevent-26.5.0-cp312-cp312-manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "1655eb04c1e20d71b2aa4a3c7528162dd58ff6cc46a037af1f01f534c80fefba", + "url": "https://files.pythonhosted.org/packages/c4/cb/98aa3a299e2fc4a2372b5d124863e02965b64579ffc29fe54d0641e65b2f/gevent-26.5.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "dc7fa28b2d627f8e87595f39043b6dec71e8e7fb97e685e5506c47cf3ff8cb2e", + "url": "https://files.pythonhosted.org/packages/da/92/1d0e7287ae55700a8d25153ac736896bd9bcc3f85a12d374ef398db4b33c/gevent-26.5.0-cp311-cp311-manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "f519139354d5ca7625df9ddb1b2ffada885c14abc5b4dbae3682e967ddf79669", + "url": "https://files.pythonhosted.org/packages/e4/32/602c499d54472f64e5cdf6013aeab5ce6aa6fed005387e8b4f2d22f5dc8d/gevent-26.5.0-cp312-cp312-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "9f463c7d6f69d13b6fe8e3b832a6175a6e95328a940f38495d25496d1ae8ad88", + "url": "https://files.pythonhosted.org/packages/e5/9a/21332674f9a10e8cdf13b41b52e9d663647a1c6e1dc3c62b07c0aeefd360/gevent-26.5.0-cp312-cp312-manylinux_2_28_ppc64le.whl" + } + ], + "project_name": "gevent", + "requires_dists": [ + "cffi>=1.17.1; platform_python_implementation == \"CPython\" and extra == \"recommended\"", + "cffi>=1.17.1; platform_python_implementation == \"CPython\" and extra == \"test\"", + "cffi>=1.17.1; platform_python_implementation == \"CPython\" and sys_platform == \"win32\"", + "coverage<7.13,>=5.0; sys_platform != \"win32\" and extra == \"test\"", + "dnspython<2.0,>=1.16.0; python_version < \"3.10\" and extra == \"dnspython\"", + "dnspython<2.0,>=1.16.0; python_version < \"3.10\" and extra == \"recommended\"", + "dnspython<2.0,>=1.16.0; python_version < \"3.10\" and extra == \"test\"", + "furo; extra == \"docs\"", + "greenlet>=3.2.2; platform_python_implementation == \"CPython\"", + "idna; python_version < \"3.10\" and extra == \"dnspython\"", + "idna; python_version < \"3.10\" and extra == \"recommended\"", + "idna; python_version < \"3.10\" and extra == \"test\"", + "objgraph; extra == \"test\"", + "psutil>=6.0.0; (sys_platform != \"win32\" or platform_python_implementation == \"CPython\") and extra == \"monitor\"", + "psutil>=6.0.0; (sys_platform != \"win32\" or platform_python_implementation == \"CPython\") and extra == \"recommended\"", + "psutil>=6.0.0; (sys_platform != \"win32\" or platform_python_implementation == \"CPython\") and extra == \"test\"", + "repoze.sphinx.autointerface; extra == \"docs\"", + "requests; extra == \"test\"", + "sphinx; extra == \"docs\"", + "sphinxcontrib-programoutput; extra == \"docs\"", + "zope.event", + "zope.interface", + "zope.schema; extra == \"docs\"" + ], + "requires_python": ">=3.9", + "version": "26.5.0" + }, { "artifacts": [ { @@ -1682,13 +1795,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d5b29685708f3c80a56db040b665bfd69492c8e150b5848532af8013b686c5a4", - "url": "https://files.pythonhosted.org/packages/f8/11/1232bb1ba52a230f20ff08e1b3df7cd9d43a71b61cc0a1c37941064fe4c7/gitpython-3.1.51-py3-none-any.whl" + "hash": "79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", + "url": "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "22c9c94bb6b0b9f3c7157c684fece45a414cea204586b600beae6cd4570dcd6d", - "url": "https://files.pythonhosted.org/packages/59/30/a8a0c15f9480dc91b5b7f11ebd26105e5f80898d7ff02da197fef35d8395/gitpython-3.1.51.tar.gz" + "hash": "de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", + "url": "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz" } ], "project_name": "gitpython", @@ -1711,7 +1824,7 @@ "typing-extensions>=3.10.0.2; python_version < \"3.10\"" ], "requires_python": ">=3.7", - "version": "3.1.51" + "version": "3.1.52" }, { "artifacts": [ @@ -1750,128 +1863,113 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", - "url": "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl" + "hash": "6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", + "url": "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", - "url": "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl" + "hash": "8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", + "url": "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", - "url": "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl" + "hash": "508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", + "url": "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" }, { "algorithm": "sha256", - "hash": "483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", - "url": "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" + "hash": "97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", + "url": "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", - "url": "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl" + "hash": "9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", + "url": "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl" }, { "algorithm": "sha256", - "hash": "0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8", - "url": "https://files.pythonhosted.org/packages/2e/8c/231ca675b0df779816950ca66b40b1fa14dbff4a0ed9814a9a29ec399140/greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" + "hash": "43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", + "url": "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", - "url": "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" + "hash": "45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", + "url": "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", - "url": "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl" + "hash": "ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", + "url": "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" }, { "algorithm": "sha256", - "hash": "719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", - "url": "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl" + "hash": "442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", + "url": "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", - "url": "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" + "hash": "8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", + "url": "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", - "url": "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" + "hash": "4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", + "url": "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" }, { "algorithm": "sha256", - "hash": "37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71", - "url": "https://files.pythonhosted.org/packages/81/fe/dd97c483a3ff82849196ccd07851600edd3ac9de74669ca8a6022ada9ea1/greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl" + "hash": "2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", + "url": "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz" }, { "algorithm": "sha256", - "hash": "215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", - "url": "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl" + "hash": "1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", + "url": "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", - "url": "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl" + "hash": "3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", + "url": "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", - "url": "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" + "hash": "ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", + "url": "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", - "url": "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" + "hash": "a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", + "url": "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", - "url": "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" + "hash": "ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", + "url": "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl" }, { "algorithm": "sha256", - "hash": "4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", - "url": "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" + "hash": "527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", + "url": "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", - "url": "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" + "hash": "c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", + "url": "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl" }, { "algorithm": "sha256", - "hash": "73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", - "url": "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl" + "hash": "a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", + "url": "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", - "url": "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl" + "hash": "b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", + "url": "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", - "url": "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", - "url": "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", - "url": "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", - "url": "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl" + "hash": "4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", + "url": "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl" } ], "project_name": "greenlet", @@ -1883,7 +1981,7 @@ "setuptools; extra == \"test\"" ], "requires_python": ">=3.10", - "version": "3.5.3" + "version": "3.3.2" }, { "artifacts": [ @@ -6615,6 +6713,125 @@ "requires_python": ">=3.10", "version": "4.1.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5e755153ac4faf64c10a4b6dd3307680166a3edf65b38df22df592610f8fa874", + "url": "https://files.pythonhosted.org/packages/9e/33/848922889e946d4befc415c219fe516af75c49555d8e736e183bfd30db42/zope_event-6.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "b97d5d6327067ee6b9dfcbdf606ade9ade70991e19c162e808ea39e5fcf0f8d3", + "url": "https://files.pythonhosted.org/packages/93/41/faa10af34d48d9cd6fa0249a1162943ad84a9590bd1a06939981e6640416/zope_event-6.2.tar.gz" + } + ], + "project_name": "zope-event", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "zope.testrunner>=6.4; extra == \"test\"" + ], + "requires_python": ">=3.10", + "version": "6.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5", + "url": "https://files.pythonhosted.org/packages/30/4c/8b56259558cace4414e753ca6740396a1f59d4a95ddb55b4658600408670/zope_interface-8.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "bf917009a4a7457c7290225a019f4a0aa706d96accd2cfdba2418d3bc1fcde2f", + "url": "https://files.pythonhosted.org/packages/03/3e/1774b0ee46ccbb5498ee3c33ece40315b6ef58bc71957be94bd345340bc1/zope_interface-8.5-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "147a9442dcc2b7339ecdb1be2b3cdb098e90462e39425054053ebfb50d99125a", + "url": "https://files.pythonhosted.org/packages/06/61/e6501d8ea7a2cac3217e03f404e1f98c1df7191d83cfe86b1895fbba5dac/zope_interface-8.5-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d", + "url": "https://files.pythonhosted.org/packages/08/dc/50550cfcbb2ea3cbca5f1d7ed05c8aa840f831a0f2d63aec0a953f7c590e/zope_interface-8.5.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "efe234a0fafb4b6b1602e9be9245b97c2bf06d67c07af5a4bc3c0438978b555c", + "url": "https://files.pythonhosted.org/packages/17/0f/8f1a29966bcf863e3a2121edcafb81c55715de7886bcc9544749cc79e7da/zope_interface-8.5-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "03bbecc7982af713d7499d4084bc03916413d17ffd45f89009348cc0c1d9e376", + "url": "https://files.pythonhosted.org/packages/37/36/7599ecabcf80ce4fef2e1ef3c5ac0d4696b61f03f724cc44022f4d226af9/zope_interface-8.5-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "c1adc90d3576b3b4c4de4953e6002c37bef28b78d7fa54c1bbfd0c50f022fe7c", + "url": "https://files.pythonhosted.org/packages/4e/78/cbceec44f1b27208a76c1a688c131302685852406a23df5aab68324109cc/zope_interface-8.5-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "a17e681224267880707c9ec9e730ad9a1ad2d65c371256843efba6cf48711b58", + "url": "https://files.pythonhosted.org/packages/91/15/bfa25ef480b02af6e9452c478483fec75e87c9e2b60c407fd0b1f6054b9c/zope_interface-8.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "8e6ee90c2e6de7c37058d5fa41f123c8b13a312db8d1e0fb5840d7f4bcdff9c9", + "url": "https://files.pythonhosted.org/packages/97/cc/b84123a948f3162a34623e188922827cd845244fdd043ed20f8d02228caa/zope_interface-8.5-cp312-cp312-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "dabeb6fe1228d411994f300811edc6866fff0cdcbc9cef98a78f05ea0da42e37", + "url": "https://files.pythonhosted.org/packages/9f/9f/37e564eaaf85e3abc1ada40a79fa43f2ab45bdb67431b0ec0fe29e4763e2/zope_interface-8.5-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "31cff25b2aaedb5267e6e77b1e9be6b0ec4f622032de8a069202b8ffacda7dc2", + "url": "https://files.pythonhosted.org/packages/b6/09/e533b2ffabaae4e5d5730d6768a591cf335defe8e37bec2ad905d09be656/zope_interface-8.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "c0c8aa2bf8f3911ef37b87deb1bbe225a310e6eb6522a16d77f5d8330c4f6fbe", + "url": "https://files.pythonhosted.org/packages/b6/43/9cd98bee951d23848de690ba2809f87e3b22c67c370987acc960da15ad37/zope_interface-8.5-cp310-cp310-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "c28044972187245d7a309e4699319bfdbd2ffcbf7176d1d4ddf5adffb2dea80f", + "url": "https://files.pythonhosted.org/packages/bb/a7/6b6e0c31ac240cb9fc015ae9ed45ca54be886c18fcf7bfa2377a4d7a8785/zope_interface-8.5-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560", + "url": "https://files.pythonhosted.org/packages/c5/66/1036543d6a66bc04c19df3cf650f3ad938a002ab0a443c24e23e8de5e8b9/zope_interface-8.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "afc66ccaef2a3c0bef6ca02aad40d29a39276389dad16a8eac36f9f385e4d057", + "url": "https://files.pythonhosted.org/packages/ce/f1/83ad110fb847413affe71609bb50e59e1aa082e1236030122227c7c283d3/zope_interface-8.5-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e", + "url": "https://files.pythonhosted.org/packages/e1/c3/005032195ff3b210c139b7c560ed5c534e844b0907d8e44d2b3d8919305e/zope_interface-8.5-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl" + } + ], + "project_name": "zope-interface", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "coverage[toml]; extra == \"test\"", + "coverage[toml]; extra == \"testing\"", + "furo; extra == \"docs\"", + "repoze.sphinx.autointerface; extra == \"docs\"", + "zope.event; extra == \"test\"", + "zope.event; extra == \"testing\"", + "zope.testing; extra == \"test\"", + "zope.testing; extra == \"testing\"" + ], + "requires_python": ">=3.10", + "version": "8.5" + }, { "artifacts": [ { @@ -6865,6 +7082,7 @@ "editor", "eventlet>=0.40.3", "flask", + "gevent", "gitdb", "gitpython", "graphviz", diff --git a/requirements-pants.txt b/requirements-pants.txt index 2f70614609..2cb21b10f3 100644 --- a/requirements-pants.txt +++ b/requirements-pants.txt @@ -19,7 +19,8 @@ openapi-spec-validator # gitpython & gitdb are used for pack management gitdb gitpython -# st2common/tests/integration/test_util_green.py requires greenlet (as does eventlet) +# st2common/tests/integration/test_util_green.py requires greenlet (as does eventlet and gevent) +gevent greenlet # Gunicorn dropped eventlet support as of v26. https://gunicorn.org/news/#2600-2026-05-05 gunicorn<26 diff --git a/requirements.txt b/requirements.txt index 4098f5e754..97fe6cb17b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,11 +18,12 @@ cryptography==43.0.3 decorator==5.2.1 dnspython editor==1.6.6 -eventlet==0.40.3 +eventlet==0.40.4 +gevent==25.9.1 gitdb==4.0.12 gitpython==3.1.45 -greenlet==3.1.1 -gunicorn<26 +greenlet==3.2.2 +gunicorn==23.0.0 importlib-metadata importlib_resources==6.5.2 jinja2==3.1.6 diff --git a/st2actions/in-requirements.txt b/st2actions/in-requirements.txt index 7a1b952619..6ad4aa4333 100644 --- a/st2actions/in-requirements.txt +++ b/st2actions/in-requirements.txt @@ -2,6 +2,7 @@ apscheduler python-dateutil eventlet +gevent jinja2 kombu oslo.config diff --git a/st2actions/requirements.txt b/st2actions/requirements.txt index afd851908b..d185048c00 100644 --- a/st2actions/requirements.txt +++ b/st2actions/requirements.txt @@ -8,7 +8,8 @@ apscheduler==3.11.0 backports.zoneinfo[tzdata]; python_version<"3.9" chardet==5.2.0 -eventlet==0.40.3 +eventlet==0.40.4 +gevent==25.9.1 gitpython==3.1.45 jinja2==3.1.6 kombu==5.5.4 diff --git a/st2actions/st2actions/policies/retry.py b/st2actions/st2actions/policies/retry.py index 225952875d..6fc546a738 100644 --- a/st2actions/st2actions/policies/retry.py +++ b/st2actions/st2actions/policies/retry.py @@ -17,9 +17,8 @@ import functools -import eventlet - from st2common import log as logging +from st2common.util import concurrency from st2common.models.db.liveaction import LiveActionDB import st2common.services.action as action_services from st2common.constants.action import LIVEACTION_STATUS_FAILED @@ -103,7 +102,7 @@ def apply_after(self, target): # status if self.delay > 0: re_run_live_action = functools.partial( - eventlet.spawn_after, + concurrency.spawn_after, self.delay, self._re_run_live_action, live_action_db=live_action_db, @@ -111,7 +110,7 @@ def apply_after(self, target): else: # Even if delay is 0, use a small delay (0.1 seconds) to prevent busy wait re_run_live_action = functools.partial( - eventlet.spawn_after, + concurrency.spawn_after, 0.1, self._re_run_live_action, live_action_db=live_action_db, diff --git a/st2actions/st2actions/scheduler/handler.py b/st2actions/st2actions/scheduler/handler.py index b6eb994f72..2150768da0 100644 --- a/st2actions/st2actions/scheduler/handler.py +++ b/st2actions/st2actions/scheduler/handler.py @@ -15,7 +15,6 @@ from __future__ import absolute_import -import eventlet import retrying from oslo_config import cfg @@ -34,6 +33,7 @@ from st2common.persistence.liveaction import LiveAction from st2common.persistence.execution_queue import ActionExecutionSchedulingQueue from st2common.util import action_db as action_utils +from st2common.util import concurrency from st2common.metrics import base as metrics from st2common.exceptions import db as db_exc @@ -51,7 +51,9 @@ class ActionExecutionSchedulingQueueHandler(object): def __init__(self): self.message_type = LiveActionDB self._shutdown = False - self._pool = eventlet.GreenPool(size=cfg.CONF.scheduler.pool_size) + self._pool = concurrency.get_green_pool_class()( + size=cfg.CONF.scheduler.pool_size + ) # If an ActionExecutionSchedulingQueueItemDB object hasn't been updated fore more than # this amount of milliseconds, it will be marked as "handled=False". # As soon as an item is picked by scheduler to be processed, it should be processed very @@ -69,7 +71,7 @@ def run(self): LOG.debug("Starting scheduler handler...") while not self._shutdown: - eventlet.greenthread.sleep(cfg.CONF.scheduler.sleep_interval) + concurrency.sleep(cfg.CONF.scheduler.sleep_interval) self.process() @retrying.retry( @@ -87,7 +89,7 @@ def cleanup(self): LOG.debug("Starting scheduler garbage collection...") while not self._shutdown: - eventlet.greenthread.sleep(cfg.CONF.scheduler.gc_interval) + concurrency.sleep(cfg.CONF.scheduler.gc_interval) self._handle_garbage_collection() def _reset_handling_flag(self): @@ -496,8 +498,8 @@ def start(self): self._shutdown = False # Spawn the worker threads. - self._main_thread = eventlet.spawn(self.run) - self._cleanup_thread = eventlet.spawn(self.cleanup) + self._main_thread = concurrency.spawn(self.run) + self._cleanup_thread = concurrency.spawn(self.cleanup) # Link the threads to the shutdown function. If either of the threads exited with error, # then initiate shutdown which will allow the waits below to throw exception to the diff --git a/st2actions/st2actions/workflows/workflows.py b/st2actions/st2actions/workflows/workflows.py index 700244d058..8384510f83 100644 --- a/st2actions/st2actions/workflows/workflows.py +++ b/st2actions/st2actions/workflows/workflows.py @@ -19,8 +19,6 @@ from orquesta import statuses from tooz.coordination import GroupNotCreated from st2common.services import coordination -from eventlet.semaphore import Semaphore -from eventlet import spawn_after from st2common.constants import action as ac_const from st2common import log as logging from st2common.metrics import base as metrics @@ -55,7 +53,7 @@ class WorkflowExecutionHandler(consumers.VariableMessageHandler): def __init__(self, connection, queues): super(WorkflowExecutionHandler, self).__init__(connection, queues) self._active_messages = 0 - self._semaphore = Semaphore() + self._semaphore = concurrency.Semaphore() # This is required to ensure workflows stuck in pausing state after shutdown transition to paused state after engine startup. self._delay = 30 @@ -106,7 +104,9 @@ def process(self, message): self._active_messages -= 1 def start(self, wait): - spawn_after(self._delay, self._resume_workflows_paused_during_shutdown) + concurrency.spawn_after( + self._delay, self._resume_workflows_paused_during_shutdown + ) super(WorkflowExecutionHandler, self).start(wait=wait) def shutdown(self): diff --git a/st2actions/tests/integration/test_actions_queue_consumer.py b/st2actions/tests/integration/test_actions_queue_consumer.py index aa37b5e589..f03b935d29 100644 --- a/st2actions/tests/integration/test_actions_queue_consumer.py +++ b/st2actions/tests/integration/test_actions_queue_consumer.py @@ -15,10 +15,11 @@ from __future__ import absolute_import import random -import eventlet from unittest import TestCase +from st2common.util import concurrency + from st2common.transport.consumers import ActionsQueueConsumer from st2common.transport.kombu import Exchange, Queue from st2common.transport.publishers import PoolPublisher @@ -44,21 +45,21 @@ def test_stop_consumption_on_shutdown(self): watcher = ActionsQueueConsumer( connection=connection, queues=queue, handler=self ) - watcher_thread = eventlet.greenthread.spawn(watcher.run) + watcher_thread = concurrency.spawn(watcher.run) # Give it some time to start up since we are publishing on a new queue - eventlet.sleep(0.5) + concurrency.sleep(0.5) body = LiveActionDB( status="scheduled", action="core.local", action_is_workflow=False ) publisher.publish(payload=body, exchange=exchange) - eventlet.sleep(0.2) + concurrency.sleep(0.2) self.assertEqual(self.message_count, 1) body = LiveActionDB( status="scheduled", action="core.local", action_is_workflow=True ) watcher.shutdown() - eventlet.sleep(1) + concurrency.sleep(1) publisher.publish(payload=body, exchange=exchange) # Second published message won't be consumed. self.assertEqual(self.message_count, 1) diff --git a/st2actions/tests/unit/test_scheduler.py b/st2actions/tests/unit/test_scheduler.py index 65a59bd869..2b9c00a2fe 100644 --- a/st2actions/tests/unit/test_scheduler.py +++ b/st2actions/tests/unit/test_scheduler.py @@ -17,7 +17,8 @@ import datetime import mock -import eventlet + +from st2common.util import concurrency import st2common from st2tests import ExecutionDbTestCase @@ -147,7 +148,7 @@ def test_next_execution(self): ) # Wait maximum delay seconds so the query works as expected - eventlet.sleep(1.0) + concurrency.sleep(1.0) for index in expected_order: test_case = test_cases[index] diff --git a/st2actions/tests/unit/test_scheduler_entrypoint.py b/st2actions/tests/unit/test_scheduler_entrypoint.py index 2862ba2b3c..9c74718319 100644 --- a/st2actions/tests/unit/test_scheduler_entrypoint.py +++ b/st2actions/tests/unit/test_scheduler_entrypoint.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import eventlet import mock +from st2common.util import concurrency + from st2actions.cmd.scheduler import _run_scheduler from st2actions.scheduler.handler import ActionExecutionSchedulingQueueHandler from st2actions.scheduler.entrypoint import SchedulerEntrypoint @@ -26,20 +27,20 @@ def mock_handler_run(self): - # NOTE: We use eventlet.sleep to emulate async nature of this process - eventlet.sleep(0.2) + # NOTE: We use concurrency.sleep to emulate async nature of this process + concurrency.sleep(0.2) raise Exception("handler run exception") def mock_handler_cleanup(self): - # NOTE: We use eventlet.sleep to emulate async nature of this process - eventlet.sleep(0.2) + # NOTE: We use concurrency.sleep to emulate async nature of this process + concurrency.sleep(0.2) raise Exception("handler clean exception") def mock_entrypoint_start(self): - # NOTE: We use eventlet.sleep to emulate async nature of this process - eventlet.sleep(0.2) + # NOTE: We use concurrency.sleep to emulate async nature of this process + concurrency.sleep(0.2) raise Exception("entrypoint start exception") @@ -47,8 +48,8 @@ class SchedulerServiceEntryPointTestCase(CleanDbTestCase): @mock.patch.object(ActionExecutionSchedulingQueueHandler, "run", mock_handler_run) @mock.patch("st2actions.cmd.scheduler.LOG") def test_service_exits_correctly_on_fatal_exception_in_handler_run(self, mock_log): - run_thread = eventlet.spawn(_run_scheduler) - result = run_thread.wait() + run_thread = concurrency.spawn(_run_scheduler) + result = concurrency.wait(run_thread) self.assertEqual(result, 1) @@ -62,8 +63,8 @@ def test_service_exits_correctly_on_fatal_exception_in_handler_run(self, mock_lo def test_service_exits_correctly_on_fatal_exception_in_handler_cleanup( self, mock_log ): - run_thread = eventlet.spawn(_run_scheduler) - result = run_thread.wait() + run_thread = concurrency.spawn(_run_scheduler) + result = concurrency.wait(run_thread) self.assertEqual(result, 1) @@ -75,8 +76,8 @@ def test_service_exits_correctly_on_fatal_exception_in_handler_cleanup( def test_service_exits_correctly_on_fatal_exception_in_entrypoint_start( self, mock_log ): - run_thread = eventlet.spawn(_run_scheduler) - result = run_thread.wait() + run_thread = concurrency.spawn(_run_scheduler) + result = concurrency.wait(run_thread) self.assertEqual(result, 1) diff --git a/st2actions/tests/unit/test_scheduler_retry.py b/st2actions/tests/unit/test_scheduler_retry.py index d975964bc6..26aed121d8 100644 --- a/st2actions/tests/unit/test_scheduler_retry.py +++ b/st2actions/tests/unit/test_scheduler_retry.py @@ -16,8 +16,9 @@ # This import must be first for import-time side-effects. from st2tests.base import CleanDbTestCase -import eventlet import mock + +from st2common.util import concurrency import pymongo import uuid @@ -25,7 +26,6 @@ from st2common.models.db import execution_queue as ex_q_db from st2common.persistence import execution_queue as ex_q_db_access - __all__ = ["SchedulerHandlerRetryTestCase"] @@ -42,38 +42,44 @@ class SchedulerHandlerRetryTestCase(CleanDbTestCase): side_effect=[pymongo.errors.ConnectionFailure(), MOCK_QUEUE_ITEM] ), ) - @mock.patch.object(eventlet.GreenPool, "spawn", mock.MagicMock(return_value=None)) + @mock.patch.object( + concurrency.get_green_pool_class(), "spawn", mock.MagicMock(return_value=None) + ) def test_handler_retry_connection_error(self): scheduling_queue_handler = handler.ActionExecutionSchedulingQueueHandler() scheduling_queue_handler.process() # Make sure retry occurs and that _handle_execution in process is called. calls = [mock.call(scheduling_queue_handler._handle_execution, MOCK_QUEUE_ITEM)] - eventlet.GreenPool.spawn.assert_has_calls(calls) + concurrency.get_green_pool_class().spawn.assert_has_calls(calls) @mock.patch.object( handler.ActionExecutionSchedulingQueueHandler, "_get_next_execution", mock.MagicMock(side_effect=[pymongo.errors.ConnectionFailure()] * 3), ) - @mock.patch.object(eventlet.GreenPool, "spawn", mock.MagicMock(return_value=None)) + @mock.patch.object( + concurrency.get_green_pool_class(), "spawn", mock.MagicMock(return_value=None) + ) def test_handler_retries_exhausted(self): scheduling_queue_handler = handler.ActionExecutionSchedulingQueueHandler() self.assertRaises( pymongo.errors.ConnectionFailure, scheduling_queue_handler.process ) - self.assertEqual(eventlet.GreenPool.spawn.call_count, 0) + self.assertEqual(concurrency.get_green_pool_class().spawn.call_count, 0) @mock.patch.object( handler.ActionExecutionSchedulingQueueHandler, "_get_next_execution", mock.MagicMock(side_effect=KeyError()), ) - @mock.patch.object(eventlet.GreenPool, "spawn", mock.MagicMock(return_value=None)) + @mock.patch.object( + concurrency.get_green_pool_class(), "spawn", mock.MagicMock(return_value=None) + ) def test_handler_retry_unexpected_error(self): scheduling_queue_handler = handler.ActionExecutionSchedulingQueueHandler() self.assertRaises(KeyError, scheduling_queue_handler.process) - self.assertEqual(eventlet.GreenPool.spawn.call_count, 0) + self.assertEqual(concurrency.get_green_pool_class().spawn.call_count, 0) @mock.patch.object( ex_q_db_access.ActionExecutionSchedulingQueue, diff --git a/st2actions/tests/unit/test_worker.py b/st2actions/tests/unit/test_worker.py index 917d0683e1..9480064244 100644 --- a/st2actions/tests/unit/test_worker.py +++ b/st2actions/tests/unit/test_worker.py @@ -16,8 +16,9 @@ from __future__ import absolute_import from bson.errors import InvalidStringData -import eventlet import mock + +from st2common.util import concurrency import os from oslo_config import cfg from tooz.drivers.redis import RedisDriver @@ -168,11 +169,11 @@ def test_worker_shutdown(self): ) liveaction_db = LiveAction.add_or_update(liveaction_db) executions.create_execution_object(liveaction_db) - runner_thread = eventlet.spawn(action_worker._run_action, liveaction_db) + runner_thread = concurrency.spawn(action_worker._run_action, liveaction_db) # Wait for the worker up to 10s to add the liveaction to _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) > 0: break @@ -231,25 +232,25 @@ def test_worker_graceful_shutdown_with_multiple_runners(self): ) liveaction_db = LiveAction.add_or_update(liveaction_db) executions.create_execution_object(liveaction_db) - runner_thread = eventlet.spawn(action_worker._run_action, liveaction_db) + runner_thread = concurrency.spawn(action_worker._run_action, liveaction_db) # Wait for the worker up to 10s to add the liveaction to _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) > 0: break self.assertEqual(len(action_worker._running_liveactions), 1) # Shutdown the worker to trigger the abandon process. - shutdown_thread = eventlet.spawn(action_worker.shutdown) + shutdown_thread = concurrency.spawn(action_worker.shutdown) # Make sure the temporary file has been deleted. self.assertFalse(os.path.isfile(temp_file)) # Wait for the worker up to 10s to remove the liveaction from _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) < 1: break liveaction_db = LiveAction.get_by_id(liveaction_db.id) @@ -294,27 +295,27 @@ def test_worker_graceful_shutdown_with_single_runner(self): ) liveaction_db = LiveAction.add_or_update(liveaction_db) executions.create_execution_object(liveaction_db) - runner_thread = eventlet.spawn(action_worker._run_action, liveaction_db) + runner_thread = concurrency.spawn(action_worker._run_action, liveaction_db) # Wait for the worker up to 10s to add the liveaction to _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) > 0: break self.assertEqual(len(action_worker._running_liveactions), 1) # Shutdown the worker to trigger the abandon process. - shutdown_thread = eventlet.spawn(action_worker.shutdown) + shutdown_thread = concurrency.spawn(action_worker.shutdown) # Wait for action runner shutdown sequence to complete - eventlet.sleep(5) + concurrency.sleep(5) # Make sure the temporary file has been deleted. self.assertFalse(os.path.isfile(temp_file)) # Wait for the worker up to 10s to remove the liveaction from _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) < 1: break liveaction_db = LiveAction.get_by_id(liveaction_db.id) @@ -360,27 +361,27 @@ def test_worker_graceful_shutdown_exit_timeout(self): ) liveaction_db = LiveAction.add_or_update(liveaction_db) executions.create_execution_object(liveaction_db) - runner_thread = eventlet.spawn(action_worker._run_action, liveaction_db) + runner_thread = concurrency.spawn(action_worker._run_action, liveaction_db) # Wait for the worker up to 10s to add the liveaction to _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) > 0: break self.assertEqual(len(action_worker._running_liveactions), 1) # Shutdown the worker to trigger the abandon process. - shutdown_thread = eventlet.spawn(action_worker.shutdown) + shutdown_thread = concurrency.spawn(action_worker.shutdown) # Continue the excution for 5+ seconds to ensure timeout occurs. - eventlet.sleep(6) + concurrency.sleep(6) # Make sure the temporary file has been deleted. self.assertFalse(os.path.isfile(temp_file)) # Wait for the worker up to 10s to remove the liveaction from _running_liveactions. for i in range(0, int(10 / 0.1)): - eventlet.sleep(0.1) + concurrency.sleep(0.1) if len(action_worker._running_liveactions) < 1: break liveaction_db = LiveAction.get_by_id(liveaction_db.id) diff --git a/st2actions/tests/unit/test_workflow_engine.py b/st2actions/tests/unit/test_workflow_engine.py index ed6440bd57..2f4344a4c4 100644 --- a/st2actions/tests/unit/test_workflow_engine.py +++ b/st2actions/tests/unit/test_workflow_engine.py @@ -15,9 +15,10 @@ from __future__ import absolute_import -import eventlet import mock +from st2common.util import concurrency + # This import must be early for import-time side-effects. import st2tests @@ -290,7 +291,7 @@ def test_process_error_handling_has_error(self, mock_get_lock): self.assertEqual(lv_ac_db.status, action_constants.LIVEACTION_STATUS_RUNNING) # Sleep up to the test config gc_max_idle_sec before running gc. - eventlet.sleep(cfg.CONF.workflow_engine.gc_max_idle_sec) + concurrency.sleep(cfg.CONF.workflow_engine.gc_max_idle_sec) # Run garbage collection. gc = garbage_collector.GarbageCollectorService() @@ -320,10 +321,10 @@ def test_workflow_engine_shutdown(self): )[0] self.assertEqual(wf_ex_db.status, action_constants.LIVEACTION_STATUS_RUNNING) workflow_engine = workflows.get_engine() - eventlet.spawn(workflow_engine.shutdown) + concurrency.spawn(workflow_engine.shutdown) # Sleep for few seconds to ensure execution transitions to pausing. - eventlet.sleep(8) + concurrency.sleep(8) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) self.assertEqual(lv_ac_db.status, action_constants.LIVEACTION_STATUS_PAUSING) @@ -349,7 +350,7 @@ def test_workflow_engine_shutdown(self): workflow_engine = workflows.get_engine() workflow_engine._delay = 0 workflow_engine.start(False) - eventlet.sleep(workflow_engine._delay + 5) + concurrency.sleep(workflow_engine._delay + 5) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) self.assertTrue( lv_ac_db.status @@ -383,10 +384,10 @@ def test_workflow_engine_shutdown_with_multiple_members(self): self.assertEqual(wf_ex_db.status, action_constants.LIVEACTION_STATUS_RUNNING) workflow_engine = workflows.get_engine() - eventlet.spawn(workflow_engine.shutdown) + concurrency.spawn(workflow_engine.shutdown) # Sleep for few seconds to ensure shutdown sequence completes. - eventlet.sleep(5) + concurrency.sleep(5) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) self.assertEqual(lv_ac_db.status, action_constants.LIVEACTION_STATUS_RUNNING) @@ -425,10 +426,10 @@ def test_workflow_engine_shutdown_with_service_registry_disabled(self): self.assertEqual(wf_ex_db.status, action_constants.LIVEACTION_STATUS_RUNNING) workflow_engine = workflows.get_engine() - eventlet.spawn(workflow_engine.shutdown) + concurrency.spawn(workflow_engine.shutdown) # Sleep for few seconds to ensure shutdown sequence completes. - eventlet.sleep(5) + concurrency.sleep(5) # WFE doesn't pause the workflow, since service registry is disabled. lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) @@ -457,11 +458,11 @@ def test_workflow_engine_shutdown_first_then_start(self): workflow_engine._delay = 5 # Initiate shutdown first - eventlet.spawn(workflow_engine.shutdown) - eventlet.spawn_after(1, workflow_engine.start, True) + concurrency.spawn(workflow_engine.shutdown) + concurrency.spawn_after(1, workflow_engine.start, True) # Sleep for few seconds to ensure shutdown sequence completes. - eventlet.sleep(2) + concurrency.sleep(2) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) # Shutdown routine acquires the lock first @@ -476,7 +477,7 @@ def test_workflow_engine_shutdown_first_then_start(self): workflows.get_engine().process(t1_ac_ex_db) # Startup sequence won't proceed until shutdown routine completes. # Assuming shutdown sequence is complete, start up sequence will resume the workflow. - eventlet.sleep(workflow_engine._delay + 5) + concurrency.sleep(workflow_engine._delay + 5) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) self.assertTrue( lv_ac_db.status @@ -514,12 +515,12 @@ def test_workflow_engine_start_first_then_shutdown(self): workflow_engine._delay = 0 # Initiate start first - eventlet.spawn(workflow_engine.start, True) - eventlet.spawn_after(1, workflow_engine.shutdown) + concurrency.spawn(workflow_engine.start, True) + concurrency.spawn_after(1, workflow_engine.shutdown) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) # Startup routine acquires the lock first and shutdown routine sees a new member present in registry. - eventlet.sleep(workflow_engine._delay + 5) + concurrency.sleep(workflow_engine._delay + 5) lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id)) self.assertEqual(lv_ac_db.status, action_constants.LIVEACTION_STATUS_RUNNING) diff --git a/st2api/in-requirements.txt b/st2api/in-requirements.txt index 25c9369f60..f2f6d5cdb3 100644 --- a/st2api/in-requirements.txt +++ b/st2api/in-requirements.txt @@ -1,5 +1,6 @@ # Remember to list implicit packages here, otherwise version won't be fixated! eventlet +gevent jsonschema kombu mongoengine diff --git a/st2api/requirements.txt b/st2api/requirements.txt index 0a4d53eeb2..580dc57d39 100644 --- a/st2api/requirements.txt +++ b/st2api/requirements.txt @@ -6,8 +6,9 @@ # in-requirements.txt for that component and then run 'make requirements' to # update the component requirements.txt backports.zoneinfo[tzdata]; python_version<"3.9" -eventlet==0.40.3 -gunicorn<26 +eventlet==0.40.4 +gevent==25.9.1 +gunicorn==23.0.0 jsonschema==4.26.0 kombu==5.5.4 mongoengine==0.29.1 diff --git a/st2api/st2api/cmd/api.py b/st2api/st2api/cmd/api.py index b80f6f4bed..1b640e0347 100644 --- a/st2api/st2api/cmd/api.py +++ b/st2api/st2api/cmd/api.py @@ -1,4 +1,4 @@ -# Copyright 2020 The StackStorm Authors. +# Copyright 2020-2026 The StackStorm Authors. # Copyright 2019 Extreme Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,9 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys - # NOTE: It's important that we perform monkey patch as early as possible before any other modules # are important, otherwise SSL support for MongoDB won't work. # See https://github.com/StackStorm/st2/issues/4832 and https://github.com/gevent/gevent/issues/1016 @@ -24,9 +21,11 @@ monkey_patch() -import eventlet +import os +import sys + from oslo_config import cfg -from eventlet import wsgi +from st2common.util import concurrency from st2common import log as logging from st2common.service_setup import setup as common_setup @@ -79,11 +78,11 @@ def _run_server(): LOG.info("(PID=%s) ST2 API is serving on http://%s:%s.", os.getpid(), host, port) - max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS - worker_pool = eventlet.GreenPool(max_pool_size) - sock = eventlet.listen((host, port)) + max_pool_size = concurrency.get_default_green_pool_size() + worker_pool = concurrency.get_green_pool_class()(max_pool_size) + sock = concurrency.listen_server(host, port) - wsgi.server( + concurrency.wsgi_server( sock, app.setup_app(), custom_pool=worker_pool, log=LOG, log_output=False ) return 0 diff --git a/st2api/tests/integration/test_gunicorn_configs.py b/st2api/tests/integration/test_gunicorn_configs.py index c7df6d3978..0ca3fee2c1 100644 --- a/st2api/tests/integration/test_gunicorn_configs.py +++ b/st2api/tests/integration/test_gunicorn_configs.py @@ -18,10 +18,12 @@ from six.moves import http_client import requests -import eventlet -from eventlet.green import subprocess import pytest +from st2common.util import concurrency + +subprocess = concurrency.get_subprocess_module() + import st2tests.config from st2common.models.utils import profiling from st2common.util.shell import kill_process @@ -48,7 +50,7 @@ def test_st2api_wsgi_entry_point(self): process = subprocess.Popen(cmd, env=env, shell=True, preexec_fn=os.setsid) try: self.add_process(process=process) - eventlet.sleep(8) + concurrency.sleep(8) self.assertProcessIsRunning(process=process) response = requests.get("http://127.0.0.1:%s/v1/actions" % (port)) self.assertEqual(response.status_code, http_client.OK) @@ -70,7 +72,7 @@ def test_st2auth(self): process = subprocess.Popen(cmd, env=env, shell=True, preexec_fn=os.setsid) try: self.add_process(process=process) - eventlet.sleep(8) + concurrency.sleep(8) self.assertProcessIsRunning(process=process) response = requests.post("http://127.0.0.1:%s/tokens" % (port)) self.assertEqual(response.status_code, http_client.UNAUTHORIZED) diff --git a/st2auth/in-requirements.txt b/st2auth/in-requirements.txt index 94e873fe41..d635ec8ddf 100644 --- a/st2auth/in-requirements.txt +++ b/st2auth/in-requirements.txt @@ -1,6 +1,7 @@ # Remember to list implicit packages here, otherwise version won't be fixated! bcrypt eventlet +gevent oslo.config pymongo six diff --git a/st2auth/requirements.txt b/st2auth/requirements.txt index 20d6f58add..4636d5bb55 100644 --- a/st2auth/requirements.txt +++ b/st2auth/requirements.txt @@ -6,8 +6,9 @@ # in-requirements.txt for that component and then run 'make requirements' to # update the component requirements.txt bcrypt==5.0.0 -eventlet==0.40.3 -gunicorn<26 +eventlet==0.40.4 +gevent==25.9.1 +gunicorn==23.0.0 oslo.config==9.6.0 pymongo==4.6.3 six==1.17.0 diff --git a/st2auth/st2auth/cmd/api.py b/st2auth/st2auth/cmd/api.py index e817765d97..27554efb64 100644 --- a/st2auth/st2auth/cmd/api.py +++ b/st2auth/st2auth/cmd/api.py @@ -1,4 +1,4 @@ -# Copyright 2020 The StackStorm Authors. +# Copyright 2020-2026 The StackStorm Authors. # Copyright 2019 Extreme Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,16 @@ monkey_patch() -import eventlet import os import sys from oslo_config import cfg -from eventlet import wsgi from st2common import log as logging from st2common.service_setup import setup as common_setup from st2common.service_setup import teardown as common_teardown from st2common.service_setup import deregister_service +from st2common.util import concurrency from st2auth import config config.register_opts(ignore_errors=True) @@ -75,29 +74,32 @@ def _run_server(): cert_file_path = os.path.realpath(cfg.CONF.auth.cert) key_file_path = os.path.realpath(cfg.CONF.auth.key) - if use_ssl and not os.path.isfile(cert_file_path): - raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) + socket = concurrency.listen(host, port) - if use_ssl and not os.path.isfile(key_file_path): - raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) + if use_ssl: + scheme = "https" + if not os.path.isfile(cert_file_path): + raise ValueError(f'Certificate file "{cert_file_path}" does not exist') - socket = eventlet.listen((host, port)) + if not os.path.isfile(key_file_path): + raise ValueError(f'Private key file "{key_file_path}" does not exist') - if use_ssl: - socket = eventlet.wrap_ssl( + socket = concurrency.wrap_ssl( socket, certfile=cert_file_path, keyfile=key_file_path, server_side=True ) + else: + scheme = "http" LOG.info('ST2 Auth API running in "%s" auth mode', cfg.CONF.auth.mode) LOG.info( "(PID=%s) ST2 Auth API is serving on %s://%s:%s.", os.getpid(), - "https" if use_ssl else "http", + scheme, host, port, ) - wsgi.server(socket, app.setup_app(), log=LOG, log_output=False) + concurrency.wsgi_server(socket, app.setup_app(), log=LOG, log_output=False) return 0 diff --git a/st2common/in-requirements.txt b/st2common/in-requirements.txt index 8ce1732fe6..e3da530e7c 100644 --- a/st2common/in-requirements.txt +++ b/st2common/in-requirements.txt @@ -3,6 +3,7 @@ apscheduler dnspython python-dateutil eventlet +gevent # used by eventlet greenlet jinja2 diff --git a/st2common/requirements.txt b/st2common/requirements.txt index 44201bfbeb..1e5fc9af48 100644 --- a/st2common/requirements.txt +++ b/st2common/requirements.txt @@ -14,10 +14,11 @@ ciso8601 cryptography==43.0.3 decorator==5.2.1 dnspython -eventlet==0.40.3 +eventlet==0.40.4 +gevent==25.9.1 gitdb==4.0.12 gitpython==3.1.45 -greenlet==3.1.1 +greenlet==3.2.2 importlib_resources==6.5.2 jinja2==3.1.6 jsonpath-rw==1.4.0 diff --git a/st2common/st2common/config.py b/st2common/st2common/config.py index a9461951d1..53b5f737c1 100644 --- a/st2common/st2common/config.py +++ b/st2common/st2common/config.py @@ -759,8 +759,8 @@ def register_opts(ignore_errors=False): cfg.BoolOpt( "use-debugger", default=True, - help="Enables debugger. Note that using this option changes how the " - "eventlet library is used to support async IO. This could result in " + help="Enables debugger. Note that using this option changes how the " + "concurrency library is used to support async IO. This could result in " "failures that do not occur under normal operation.", ), cfg.BoolOpt( @@ -769,14 +769,14 @@ def register_opts(ignore_errors=False): help="Enable code profiler mode. Do not use in production.", ), cfg.BoolOpt( - "enable-eventlet-blocking-detection", + "enable-concurrency-blocking-detection", default=False, - help="Enable eventlet blocking detection logic. Do not use in production.", + help="Enable blocking detection logic in concurrency library. Do not use in production.", ), cfg.FloatOpt( - "eventlet-blocking-detection-resolution", + "concurrency-blocking-detection-resolution", default=0.5, - help="Resolution in seconds for eventlet blocking detection logic.", + help="Resolution in seconds for blocking detection logic in currency library.", ), ] diff --git a/st2common/st2common/service_setup.py b/st2common/st2common/service_setup.py index 9e8ae7e07f..c8fb4527d0 100644 --- a/st2common/st2common/service_setup.py +++ b/st2common/st2common/service_setup.py @@ -1,4 +1,4 @@ -# Copyright 2020 The StackStorm Authors. +# Copyright 2020-2026 The StackStorm Authors. # Copyright 2019 Extreme Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,7 +26,7 @@ import logging as stdlib_logging import six -import eventlet.debug + from oslo_config import cfg from tooz.coordination import GroupAlreadyExist from tooz.coordination import GroupNotCreated @@ -38,6 +38,7 @@ from st2common.transport.bootstrap_utils import register_kombu_serializers from st2common.bootstrap import runnersregistrar from st2common.signal_handlers import register_common_signal_handlers +from st2common.util.concurrency import blocking_detection from st2common.util.debugging import enable_debugging from st2common.models.utils.profiling import enable_profiling from st2common import triggers @@ -285,11 +286,10 @@ def setup( # modules like jinja, stevedore, etc load files from disk on init which is slow and will be # detected as blocking operation, but this is not really an issue inside the service startup / # init phase. - if cfg.CONF.enable_eventlet_blocking_detection: - print("Eventlet long running / blocking operation detection logic enabled") - print(cfg.CONF.eventlet_blocking_detection_resolution) - eventlet.debug.hub_blocking_detection( - state=True, resolution=cfg.CONF.eventlet_blocking_detection_resolution + if cfg.CONF.enable_concurrency_blocking_detection: + blocking_detection( + cfg.CONF.enable_concurrency_blocking_detection, + cfg.CONF.concurrency_blocking_detection_resolution, ) diff --git a/st2common/st2common/stream/listener.py b/st2common/st2common/stream/listener.py index 5eb5dc8b8f..b79c2c477f 100644 --- a/st2common/st2common/stream/listener.py +++ b/st2common/st2common/stream/listener.py @@ -16,7 +16,7 @@ from __future__ import absolute_import import fnmatch -import eventlet +from st2common.util import concurrency from kombu.mixins import ConsumerMixin from oslo_config import cfg @@ -94,7 +94,7 @@ def generator( end_statuses=None, end_execution_id=None, ): - queue = eventlet.Queue() + queue = concurrency.Queue() queue.put("") self.queues.append(queue) try: @@ -144,7 +144,7 @@ def generator( continue yield message - except eventlet.queue.Empty: + except concurrency.get_queue_empty_exception(): yield finally: self.queues.remove(queue) @@ -273,13 +273,13 @@ def get_listener(name): if not _stream_listener: with transport_utils.get_connection() as conn: _stream_listener = StreamListener(conn) - eventlet.spawn_n(listen, _stream_listener) + concurrency.spawn(listen, _stream_listener) return _stream_listener elif name == "execution_output": if not _execution_output_listener: with transport_utils.get_connection() as conn: _execution_output_listener = ExecutionOutputListener(conn) - eventlet.spawn_n(listen, _execution_output_listener) + concurrency.spawn(listen, _execution_output_listener) return _execution_output_listener else: raise ValueError("Invalid listener name: %s" % (name)) diff --git a/st2common/st2common/util/concurrency.py b/st2common/st2common/util/concurrency.py index f5a2212e43..0111227f41 100644 --- a/st2common/st2common/util/concurrency.py +++ b/st2common/st2common/util/concurrency.py @@ -1,4 +1,4 @@ -# Copyright 2020 The StackStorm Authors. +# Copyright 2020-2026 The StackStorm Authors. # Copyright 2019 Extreme Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,9 +17,11 @@ Module which acts as a compatibility later between eventlet and gevent. It dispatches function call to the concurrency library which is configured using -"set_concurrency_library" functoion. +"set_concurrency_library" function. """ +import os + try: import eventlet # pylint: disable=import-error except ImportError: @@ -27,11 +29,13 @@ try: import gevent # pylint: disable=import-error # pants: no-infer-dep + import gevent.lock import gevent.pool + import gevent.queue except ImportError: gevent = None -CONCURRENCY_LIBRARY = "eventlet" +CONCURRENCY_LIBRARY = os.environ.get("ST2_CONCURRENCY_LIBRARY", "eventlet") __all__ = [ "set_concurrency_library", @@ -73,6 +77,32 @@ def get_subprocess_module(): from gevent import subprocess # pylint: disable=import-error return subprocess + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def wsgi_server( + socket, app, custom_pool=None, log=None, log_output=True, *args, **kwargs +): + if CONCURRENCY_LIBRARY == "eventlet": + from eventlet import wsgi + + wsgi.server( + socket, + app, + custom_pool=custom_pool, + log=log, + log_output=log_output, + *args, + **kwargs, + ) + elif CONCURRENCY_LIBRARY == "gevent": + from gevent import pywsgi + + server = pywsgi.WSGIServer(socket, app, spawn=custom_pool, log=log) + server.serve_forever() + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def subprocess_popen(*args, **kwargs): @@ -84,6 +114,26 @@ def subprocess_popen(*args, **kwargs): from gevent import subprocess # pylint: disable=import-error return subprocess.Popen(*args, **kwargs) + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def spawn_after(seconds, func, *args, **kwargs): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.spawn_after(seconds, func, *args, **kwargs) + elif CONCURRENCY_LIBRARY == "gevent": + return gevent.spawn_later(seconds, func, *args, **kwargs) + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def Semaphore(*args, **kwargs): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.Semaphore(*args, **kwargs) + elif CONCURRENCY_LIBRARY == "gevent": + return gevent.lock.Semaphore(*args, **kwargs) + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def spawn(func, *args, **kwargs): @@ -92,7 +142,7 @@ def spawn(func, *args, **kwargs): elif CONCURRENCY_LIBRARY == "gevent": return gevent.spawn(func, *args, **kwargs) else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def wait(green_thread, *args, **kwargs): @@ -101,7 +151,7 @@ def wait(green_thread, *args, **kwargs): elif CONCURRENCY_LIBRARY == "gevent": return green_thread.join(*args, **kwargs) else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def cancel(green_thread, *args, **kwargs): @@ -110,7 +160,7 @@ def cancel(green_thread, *args, **kwargs): elif CONCURRENCY_LIBRARY == "gevent": return green_thread.kill(*args, **kwargs) else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def kill(green_thread, *args, **kwargs): @@ -119,7 +169,29 @@ def kill(green_thread, *args, **kwargs): elif CONCURRENCY_LIBRARY == "gevent": return green_thread.kill(*args, **kwargs) else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def listen(host, port): + return listen_server(host, port) + + +def Queue(*args, **kwargs): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.Queue(*args, **kwargs) + elif CONCURRENCY_LIBRARY == "gevent": + return gevent.queue.Queue(*args, **kwargs) + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def get_queue_empty_exception(): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.queue.Empty + elif CONCURRENCY_LIBRARY == "gevent": + return gevent.queue.Empty + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def sleep(*args, **kwargs): @@ -128,7 +200,7 @@ def sleep(*args, **kwargs): elif CONCURRENCY_LIBRARY == "gevent": return gevent.sleep(*args, **kwargs) else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") def get_greenlet_exit_exception_class(): @@ -136,6 +208,16 @@ def get_greenlet_exit_exception_class(): return eventlet.support.greenlets.GreenletExit elif CONCURRENCY_LIBRARY == "gevent": return gevent.GreenletExit + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def get_default_green_pool_size(): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS + elif CONCURRENCY_LIBRARY == "gevent": + # matches what DEFAULT_MAX_SIMULTANEOUS_REQUESTS is for eventlet + return 1024 else: raise ValueError("Unsupported concurrency library") @@ -146,17 +228,48 @@ def get_green_pool_class(): elif CONCURRENCY_LIBRARY == "gevent": return gevent.pool.Pool else: - raise ValueError("Unsupported concurrency library") + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") -def is_green_pool_free(pool): +def green_pool_free_count(pool): """ - Return True if the provided green pool is free, False otherwise. + Return the number of free slots in the pool. """ if CONCURRENCY_LIBRARY == "eventlet": return pool.free() elif CONCURRENCY_LIBRARY == "gevent": - return not pool.full() + return pool.free_count() + else: + raise ValueError(f"Unsupported concurrency library {CONCURRENCY_LIBRARY}") + + +def is_green_pool_free(pool): + """ + Return True if the provided green pool has at least one free slot, False otherwise. + """ + return green_pool_free_count(pool) > 0 + + +def green_pool_running_count(pool): + """ + Return the number of greenlets currently running in the pool. + """ + if CONCURRENCY_LIBRARY == "eventlet": + return pool.running() + elif CONCURRENCY_LIBRARY == "gevent": + return len(pool.greenlets) + else: + raise ValueError("Unsupported concurrency library") + + +def get_pool_greenlets(pool): + """ + Return the set of currently running greenlets in the pool. + """ + if CONCURRENCY_LIBRARY == "eventlet": + return pool.coroutines_running + elif CONCURRENCY_LIBRARY == "gevent": + return pool.greenlets else: raise ValueError("Unsupported concurrency library") @@ -168,8 +281,56 @@ def green_pool_wait_all(pool): if CONCURRENCY_LIBRARY == "eventlet": return pool.waitall() elif CONCURRENCY_LIBRARY == "gevent": - # NOTE: This mimicks eventlet.waitall() functionallity better than + # NOTE: This mimicks eventlet.waitall() functionality better than # pool.join() return all(gl.ready() for gl in pool.greenlets) else: raise ValueError("Unsupported concurrency library") + + +def listen_server(host, port, backlog=50, **kwargs): + """ + Start listening on the host:port. + :backlog: the number of unaccepted connections that the system will allow before refusing new connections. + """ + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.listen((host, port), backlog=backlog, **kwargs) + elif CONCURRENCY_LIBRARY == "gevent": + import socket + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((host, port)) + sock.listen(backlog) + return sock + else: + raise ValueError("Unsupported concurrency library") + + +def wrap_ssl(socket, *args, **kwargs): + if CONCURRENCY_LIBRARY == "eventlet": + return eventlet.wrap_ssl(socket, *args, **kwargs) + elif CONCURRENCY_LIBRARY == "gevent": + # Monkey patching in the caller module is required prior to + # calling wrap_ssl() or this may block. + import ssl + + return ssl.wrap_socket(socket, *args, **kwargs) + else: + raise ValueError("Unsupported concurrency library") + + +def blocking_detection(enable=False, timeout=1.0): + if CONCURRENCY_LIBRARY == "eventlet": + print( + f"Eventlet long running / blocking operation detection logic enabled. Block timeout ({timeout})." + ) + eventlet.debug.hub_blocking_detection(state=enable, resolution=timeout) + elif CONCURRENCY_LIBRARY == "gevent": + print( + f"gEvent long running / blocking operation detection logic enabled. Block timeout ({timeout})." + ) + gevent.config.monitor_thread = enable + gevent.config.max_blocking_time = timeout + else: + raise ValueError("Unsupported concurrency library") diff --git a/st2common/st2common/util/greenpooldispatch.py b/st2common/st2common/util/greenpooldispatch.py index 156d530116..4ad5eab872 100644 --- a/st2common/st2common/util/greenpooldispatch.py +++ b/st2common/st2common/util/greenpooldispatch.py @@ -16,9 +16,10 @@ from __future__ import absolute_import import time -import eventlet import six.moves.queue +from st2common.util import concurrency + from st2common import log as logging __all__ = ["BufferedDispatcher"] @@ -44,8 +45,8 @@ def __init__( name=None, ): self._pool_limit = dispatch_pool_size - self._dispatcher_pool = eventlet.GreenPool(dispatch_pool_size) - self._dispatch_monitor_thread = eventlet.greenthread.spawn(self._flush) + self._dispatcher_pool = concurrency.get_green_pool_class()(dispatch_pool_size) + self._dispatch_monitor_thread = concurrency.spawn(self._flush) self._monitor_thread_empty_q_sleep_time = monitor_thread_empty_q_sleep_time self._monitor_thread_no_workers_sleep_time = ( monitor_thread_no_workers_sleep_time @@ -71,13 +72,13 @@ def shutdown(self): def _flush(self): while True: while self._work_buffer.empty(): - eventlet.greenthread.sleep(self._monitor_thread_empty_q_sleep_time) - while self._dispatcher_pool.free() <= 0: - eventlet.greenthread.sleep(self._monitor_thread_no_workers_sleep_time) + concurrency.sleep(self._monitor_thread_empty_q_sleep_time) + while not concurrency.is_green_pool_free(self._dispatcher_pool): + concurrency.sleep(self._monitor_thread_no_workers_sleep_time) self._flush_now() def _flush_now(self): - if self._dispatcher_pool.free() <= 0: + if not concurrency.is_green_pool_free(self._dispatcher_pool): now = time.time() if (now - self._pool_last_free_ts) >= POOL_BUSY_THRESHOLD_SECONDS: @@ -90,12 +91,14 @@ def _flush_now(self): # Update the time of when there were free threads available self._pool_last_free_ts = time.time() - while not self._work_buffer.empty() and self._dispatcher_pool.free() > 0: + while not self._work_buffer.empty() and concurrency.is_green_pool_free( + self._dispatcher_pool + ): (handler, args) = self._work_buffer.get_nowait() self._dispatcher_pool.spawn(handler, *args) def __repr__(self): - free_count = self._dispatcher_pool.free() + free_count = concurrency.green_pool_free_count(self._dispatcher_pool) values = ( self.name, self._pool_limit, diff --git a/st2common/st2common/util/monkey_patch.py b/st2common/st2common/util/monkey_patch.py index ceb430592d..79c63582de 100644 --- a/st2common/st2common/util/monkey_patch.py +++ b/st2common/st2common/util/monkey_patch.py @@ -18,6 +18,7 @@ """ from __future__ import absolute_import +from st2common.util.concurrency import get_concurrency_library import os import sys @@ -45,15 +46,25 @@ def monkey_patch(patch_thread=None): patched unless debugger is used. :type patch_thread: ``bool`` """ - import eventlet + concurrency_library = get_concurrency_library() + if concurrency_library == "eventlet": + import eventlet - if patch_thread is None: - patch_thread = not is_use_debugger_flag_provided() + if patch_thread is None: + patch_thread = not is_use_debugger_flag_provided() - # TODO: support gevent.patch_all if .concurrency.CONCURRENCY_LIBRARY = "gevent" - eventlet.monkey_patch( - os=True, select=True, socket=True, thread=patch_thread, time=True - ) + eventlet.monkey_patch( + os=True, select=True, socket=True, thread=patch_thread, time=True + ) + elif concurrency_library == "gevent": + # Match what eventlet was enabling, fallback on gevent defaults + import gevent + + gevent.monkey.patch_all( + os=True, select=True, thread=True, time=True, socket=True + ) + else: + raise RuntimeError(f"Unsupported concurrency library {concurrency_library}") def use_select_poll_workaround(nose_only=True): @@ -79,30 +90,31 @@ def use_select_poll_workaround(nose_only=True): runner. :type nose_only: ``bool`` """ - import sys - import select - import subprocess - import eventlet - - # Work around to get tests to pass with eventlet >= 0.20.0 - if not nose_only or ( - nose_only - # sys._called_from_test set in conftest.py for pytest runs - and ("nose" in sys.modules.keys() or hasattr(sys, "_called_from_test")) - ): - # Add back blocking poll() to eventlet monkeypatched select - original_poll = eventlet.patcher.original("select").poll - select.poll = original_poll - - sys.modules["select"] = select - subprocess.select = select - - if sys.version_info >= (3, 6, 5): - # If we also don't patch selectors.select, it will fail with Python >= 3.6.5 - import selectors # pylint: disable=import-error - - sys.modules["selectors"] = selectors - selectors.select = sys.modules["select"] + if get_concurrency_library() == "eventlet": + import sys + import select + import subprocess + import eventlet + + # Work around to get tests to pass with eventlet >= 0.20.0 + if not nose_only or ( + nose_only + # sys._called_from_test set in conftest.py for pytest runs + and ("nose" in sys.modules.keys() or hasattr(sys, "_called_from_test")) + ): + # Add back blocking poll() to eventlet monkeypatched select + original_poll = eventlet.patcher.original("select").poll + select.poll = original_poll + + sys.modules["select"] = select + subprocess.select = select + + if sys.version_info >= (3, 6, 5): + # If we also don't patch selectors.select, it will fail with Python >= 3.6.5 + import selectors # pylint: disable=import-error + + sys.modules["selectors"] = selectors + selectors.select = sys.modules["select"] def is_use_debugger_flag_provided(): diff --git a/st2common/st2common/util/shell.py b/st2common/st2common/util/shell.py index 171085be74..7259f180ad 100644 --- a/st2common/st2common/util/shell.py +++ b/st2common/st2common/util/shell.py @@ -124,9 +124,9 @@ def kill_process(process): try: if six.PY3: - status = subprocess.call( + status = subprocess.call( # pylint: disable=not-callable kill_command, timeout=100 - ) # pylint: disable=not-callable + ) else: status = subprocess.call(kill_command) # pylint: disable=not-callable except Exception: diff --git a/st2common/st2common/util/wsgi.py b/st2common/st2common/util/wsgi.py index 63ec6c6253..b7b77f3ba5 100644 --- a/st2common/st2common/util/wsgi.py +++ b/st2common/st2common/util/wsgi.py @@ -14,13 +14,13 @@ # limitations under the License. """ -Eventlet WSGI server related utility functions. +WSGI server related utility functions. """ from __future__ import absolute_import -import eventlet from st2common import log as logging +from st2common.util import concurrency LOG = logging.getLogger(__name__) @@ -43,17 +43,17 @@ def shutdown_server_kill_pending_requests(sock, worker_pool, wait_time=2): worker_pool.resize(0) sock.close() - active_requests = worker_pool.running() + active_requests = concurrency.green_pool_running_count(worker_pool) LOG.info("Shutting down. Requests left: %s", active_requests) # Give active requests some time to finish if active_requests > 0: - eventlet.sleep(wait_time) + concurrency.sleep(wait_time) # Kill requests which still didn't finish - running_corutines = worker_pool.coroutines_running.copy() - for coro in running_corutines: - eventlet.greenthread.kill(coro) + running_greenlets = concurrency.get_pool_greenlets(worker_pool).copy() + for coro in running_greenlets: + concurrency.kill(coro) LOG.info("Exiting...") raise SystemExit() diff --git a/st2common/tests/integration/test_logging.py b/st2common/tests/integration/test_logging.py index 87852914d9..019987f06e 100644 --- a/st2common/tests/integration/test_logging.py +++ b/st2common/tests/integration/test_logging.py @@ -26,8 +26,9 @@ import signal import pytest -import eventlet -from eventlet.green import subprocess +from st2common.util import concurrency + +subprocess = concurrency.get_subprocess_module() from st2tests.base import IntegrationTestCase @@ -55,7 +56,7 @@ def test_formatting_with_unicode_data_works_no_stdout_patching_valid_utf8_encodi self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8").strip() @@ -98,7 +99,7 @@ def test_formatting_with_unicode_data_works_no_stdout_patching_non_valid_utf8_en self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8") @@ -144,7 +145,7 @@ def test_formatting_with_unicode_data_works_no_stdout_patching_ascii_pythonioenc self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8") @@ -188,7 +189,7 @@ def test_formatting_with_unicode_data_works_with_stdout_patching_valid_utf8_enco self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8") @@ -224,7 +225,7 @@ def test_formatting_with_unicode_data_works_with_stdout_patching_non_valid_utf8_ self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8") @@ -261,7 +262,7 @@ def test_formatting_with_unicode_data_works_with_stdout_patching__ascii_pythonio self.add_process(process=process) # Give it some time to start up and run for a while - eventlet.sleep(2) + concurrency.sleep(2) process.send_signal(signal.SIGKILL) stdout = process.stdout.read().decode("utf-8") diff --git a/st2common/tests/integration/test_service_setup_log_level_filtering.py b/st2common/tests/integration/test_service_setup_log_level_filtering.py index f55214212a..cf5d3b87c4 100644 --- a/st2common/tests/integration/test_service_setup_log_level_filtering.py +++ b/st2common/tests/integration/test_service_setup_log_level_filtering.py @@ -18,8 +18,9 @@ import sys import signal -import eventlet -from eventlet.green import subprocess +from st2common.util import concurrency + +subprocess = concurrency.get_subprocess_module() import st2tests.config from st2tests.base import IntegrationTestCase @@ -83,7 +84,7 @@ def test_system_info_is_logged_on_startup(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # Verify first 4 environment related log messages @@ -105,7 +106,7 @@ def test_warning_is_emitted_on_non_utf8_encoding(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # Verify first 4 environment related log messages @@ -126,7 +127,7 @@ def test_audit_log_level_is_filtered_if_log_level_is_not_debug_or_audit(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # Verify first 4 environment related log messages @@ -143,7 +144,7 @@ def test_audit_log_level_is_filtered_if_log_level_is_not_debug_or_audit(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # First 6 log lines are debug messages about the environment which are always logged @@ -158,7 +159,7 @@ def test_audit_log_level_is_filtered_if_log_level_is_not_debug_or_audit(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # First 6 log lines are debug messages about the environment which are always logged @@ -173,7 +174,7 @@ def test_audit_log_level_is_filtered_if_log_level_is_not_debug_or_audit(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # First 6 log lines are debug messages about the environment which are always logged @@ -188,7 +189,7 @@ def test_audit_log_level_is_filtered_if_log_level_is_not_debug_or_audit(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) # First 6 log lines are debug messages about the environment which are always logged @@ -207,7 +208,7 @@ def test_kombu_heartbeat_tick_log_messages_are_excluded(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) stdout = "\n".join(process.stdout.read().decode("utf-8").split("\n")) @@ -218,7 +219,7 @@ def test_kombu_heartbeat_tick_log_messages_are_excluded(self): self.add_process(process=process) # Give it some time to start up - eventlet.sleep(8) + concurrency.sleep(8) process.send_signal(signal.SIGTERM) stdout = "\n".join(process.stdout.read().decode("utf-8").split("\n")) diff --git a/st2common/tests/unit/test_greenpooldispatch.py b/st2common/tests/unit/test_greenpooldispatch.py index f77ae2937e..8ad17c2b17 100644 --- a/st2common/tests/unit/test_greenpooldispatch.py +++ b/st2common/tests/unit/test_greenpooldispatch.py @@ -14,9 +14,9 @@ # limitations under the License. from __future__ import absolute_import -import eventlet import mock +from st2common.util import concurrency from st2common.util.greenpooldispatch import BufferedDispatcher from unittest import TestCase from six.moves import range @@ -31,7 +31,7 @@ def test_dispatch_simple(self): dispatcher.dispatch(mock_handler, i, i + 1) expected.append((i, i + 1)) while mock_handler.call_count < 10: - eventlet.sleep(0.01) + concurrency.sleep(0.01) dispatcher.shutdown() call_args_list = [ (args[0][0], args[0][1]) for args in mock_handler.call_args_list @@ -50,7 +50,7 @@ def test_dispatch_starved(self): dispatcher.dispatch(mock_handler, i, i + 1) expected.append((i, i + 1)) while mock_handler.call_count < 10: - eventlet.sleep(0.01) + concurrency.sleep(0.01) dispatcher.shutdown() call_args_list = [ (args[0][0], args[0][1]) for args in mock_handler.call_args_list diff --git a/st2common/tests/unit/test_stream_generator.py b/st2common/tests/unit/test_stream_generator.py index 39a67621b5..ad5997d7ed 100644 --- a/st2common/tests/unit/test_stream_generator.py +++ b/st2common/tests/unit/test_stream_generator.py @@ -54,7 +54,7 @@ def get_consumers(self, consumer, channel): class TestStream(unittest.TestCase): @mock.patch("st2common.stream.listener.BaseListener._get_action_ref_for_body") - @mock.patch("eventlet.Queue") + @mock.patch("st2common.util.concurrency.Queue") def test_generator(self, mock_queue, get_action_ref_for_body): get_action_ref_for_body.return_value = None mock_queue.return_value = MockQueue() diff --git a/st2common/tests/unit/test_transport.py b/st2common/tests/unit/test_transport.py index 71d18e6010..abade10e0d 100644 --- a/st2common/tests/unit/test_transport.py +++ b/st2common/tests/unit/test_transport.py @@ -21,7 +21,8 @@ import random import unittest -import eventlet + +from st2common.util import concurrency from bson.objectid import ObjectId from kombu.mixins import ConsumerMixin @@ -77,16 +78,16 @@ def test_publish_compression(self): with transport_utils.get_connection() as connection: connection.connect() watcher = QueueConsumer(connection=connection, queue=queue) - watcher_thread = eventlet.greenthread.spawn(watcher.run) + watcher_thread = concurrency.spawn(watcher.run) # Give it some time to start up since we are publishing on a new queue - eventlet.sleep(0.5) + concurrency.sleep(0.5) self.assertEqual(len(watcher.received_messages), 0) # 1. Verify compression is off as a default publisher.publish(payload=live_action_db, exchange=exchange) - eventlet.sleep(0.2) + concurrency.sleep(0.2) self.assertEqual(len(watcher.received_messages), 1) self.assertEqual( @@ -105,7 +106,7 @@ def test_publish_compression(self): cfg.CONF.set_override(name="compression", group="messaging", override="zstd") publisher.publish(payload=live_action_db, exchange=exchange) - eventlet.sleep(0.2) + concurrency.sleep(0.2) self.assertEqual(len(watcher.received_messages), 2) self.assertEqual( @@ -125,7 +126,7 @@ def test_publish_compression(self): cfg.CONF.set_override(name="compression", group="messaging", override="zstd") publisher.publish(payload=live_action_db, exchange=exchange, compression="gzip") - eventlet.sleep(0.2) + concurrency.sleep(0.2) self.assertEqual(len(watcher.received_messages), 3) self.assertEqual( diff --git a/st2reactor/in-requirements.txt b/st2reactor/in-requirements.txt index e1bccfac12..a2865114e9 100644 --- a/st2reactor/in-requirements.txt +++ b/st2reactor/in-requirements.txt @@ -2,6 +2,7 @@ apscheduler python-dateutil eventlet +gevent jsonpath-rw jsonschema kombu diff --git a/st2reactor/requirements.txt b/st2reactor/requirements.txt index 2442b957bc..fe0c7fb174 100644 --- a/st2reactor/requirements.txt +++ b/st2reactor/requirements.txt @@ -7,7 +7,8 @@ # update the component requirements.txt apscheduler==3.11.0 backports.zoneinfo[tzdata]; python_version<"3.9" -eventlet==0.40.3 +eventlet==0.40.4 +gevent==25.9.1 jsonpath-rw==1.4.0 jsonschema==4.26.0 kombu==5.5.4 diff --git a/st2reactor/tests/unit/test_sensor_wrapper.py b/st2reactor/tests/unit/test_sensor_wrapper.py index 36b8c04f10..16ae747d23 100644 --- a/st2reactor/tests/unit/test_sensor_wrapper.py +++ b/st2reactor/tests/unit/test_sensor_wrapper.py @@ -26,6 +26,8 @@ import mock import eventlet +from st2common.util import concurrency + import st2tests.config as tests_config from st2tests.base import TESTS_CONFIG_PATH @@ -203,6 +205,11 @@ def test_sensor_wrapper_poll_method_still_works(self): # 'poll'" will be thrown import select - self.assertTrue(eventlet.patcher.is_monkey_patched(select)) - self.assertTrue(select != eventlet.patcher.original("select")) + if concurrency.get_concurrency_library() == "eventlet": + self.assertTrue(eventlet.patcher.is_monkey_patched(select)) + self.assertTrue(select != eventlet.patcher.original("select")) + else: + import gevent.monkey + + self.assertTrue(gevent.monkey.is_module_patched("select")) self.assertTrue(select.poll()) diff --git a/st2stream/in-requirements.txt b/st2stream/in-requirements.txt index 273a2b1b2f..7671b1712b 100644 --- a/st2stream/in-requirements.txt +++ b/st2stream/in-requirements.txt @@ -1,5 +1,6 @@ # Remember to list implicit packages here, otherwise version won't be fixated! eventlet +gevent jsonschema kombu mongoengine diff --git a/st2stream/requirements.txt b/st2stream/requirements.txt index 184c7f79e4..6cdc4670cf 100644 --- a/st2stream/requirements.txt +++ b/st2stream/requirements.txt @@ -6,8 +6,9 @@ # in-requirements.txt for that component and then run 'make requirements' to # update the component requirements.txt backports.zoneinfo[tzdata]; python_version<"3.9" -eventlet==0.40.3 -gunicorn<26 +eventlet==0.40.4 +gevent==25.9.1 +gunicorn==23.0.0 jsonschema==4.26.0 kombu==5.5.4 mongoengine==0.29.1 diff --git a/st2stream/st2stream/cmd/api.py b/st2stream/st2stream/cmd/api.py index 3de0c89f03..cbc3cd11d7 100644 --- a/st2stream/st2stream/cmd/api.py +++ b/st2stream/st2stream/cmd/api.py @@ -1,4 +1,4 @@ -# Copyright 2020 The StackStorm Authors. +# Copyright 2020-2026 The StackStorm Authors. # Copyright 2019 Extreme Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,15 +20,14 @@ import os import sys -import eventlet from oslo_config import cfg -from eventlet import wsgi from st2common import log as logging from st2common.service_setup import setup as common_setup from st2common.service_setup import teardown as common_teardown from st2common.service_setup import deregister_service from st2common.stream.listener import get_listener_if_set +from st2common.util import concurrency from st2common.util.wsgi import shutdown_server_kill_pending_requests from st2stream.signal_handlers import register_stream_signal_handlers from st2stream import config @@ -40,14 +39,6 @@ __all__ = ["main"] -eventlet.monkey_patch( - os=True, - select=True, - socket=True, - thread=False if "--use-debugger" in sys.argv else True, - time=True, -) - LOG = logging.getLogger(__name__) STREAM = "stream" @@ -76,6 +67,7 @@ def _setup(): def _run_server(): + host = cfg.CONF.stream.host port = cfg.CONF.stream.port @@ -83,13 +75,13 @@ def _run_server(): "(PID=%s) ST2 Stream API is serving on http://%s:%s.", os.getpid(), host, port ) - max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS - worker_pool = eventlet.GreenPool(max_pool_size) - sock = eventlet.listen((host, port)) + max_pool_size = concurrency.get_default_green_pool_size() + worker_pool = concurrency.get_green_pool_class()(max_pool_size) + sock = concurrency.listen_server(host, port) def queue_shutdown(signal_number, stack_frame): deregister_service(STREAM) - eventlet.spawn_n( + concurrency.spawn( shutdown_server_kill_pending_requests, sock=sock, worker_pool=worker_pool, @@ -101,7 +93,7 @@ def queue_shutdown(signal_number, stack_frame): # will still want to kill long running stream requests. register_stream_signal_handlers(handler_func=queue_shutdown) - wsgi.server(sock, app.setup_app(), custom_pool=worker_pool) + concurrency.wsgi_server(sock, app.setup_app(), custom_pool=worker_pool) return 0 diff --git a/st2stream/tests/unit/controllers/v1/test_stream_execution_output.py b/st2stream/tests/unit/controllers/v1/test_stream_execution_output.py index 9a135d1789..fe8116f80a 100644 --- a/st2stream/tests/unit/controllers/v1/test_stream_execution_output.py +++ b/st2stream/tests/unit/controllers/v1/test_stream_execution_output.py @@ -15,7 +15,7 @@ import json -import eventlet +from st2common.util import concurrency from six.moves import http_client @@ -48,7 +48,7 @@ def test_get_output_running_execution(self): # early enough for tests to pass. # NOTE: This only affects tests where listeners are not pre-initialized. listener = get_listener(name="execution_output") - eventlet.sleep(0.5) + concurrency.sleep(0.5) # Test the execution output API endpoint for execution which is running (blocking) status = action_constants.LIVEACTION_STATUS_RUNNING @@ -89,14 +89,14 @@ def publish_action_finished(action_execution_db): output_db = ActionExecutionOutputDB(**output_params) ActionExecutionOutput.add_or_update(output_db) - eventlet.sleep(0.5) + concurrency.sleep(0.5) # Transition execution to completed state so the connection closes action_execution_db.status = action_constants.LIVEACTION_STATUS_SUCCEEDED action_execution_db = ActionExecution.add_or_update(action_execution_db) - eventlet.spawn_after(0.2, insert_mock_data) - eventlet.spawn_after(1.0, publish_action_finished, action_execution_db) + concurrency.spawn_after(0.2, insert_mock_data) + concurrency.spawn_after(1.0, publish_action_finished, action_execution_db) # Retrieve data while execution is running - endpoint return new data once it's available # and block until the execution finishes diff --git a/st2tests/st2tests/base.py b/st2tests/st2tests/base.py index b452f79983..d28f634239 100644 --- a/st2tests/st2tests/base.py +++ b/st2tests/st2tests/base.py @@ -38,6 +38,7 @@ import six import eventlet import psutil +from st2common.util import concurrency import mock from oslo_config import cfg from unittest import TestCase @@ -186,19 +187,14 @@ class EventletTestCase(TestCase): @classmethod def setUpClass(cls): - eventlet.monkey_patch( - os=True, - select=True, - socket=True, - thread=False if "--use-debugger" in sys.argv else True, - time=True, - ) + monkey_patch() @classmethod def tearDownClass(cls): - eventlet.monkey_patch( - os=False, select=False, socket=False, thread=False, time=False - ) + if concurrency.get_concurrency_library() == "eventlet": + eventlet.monkey_patch( + os=False, select=False, socket=False, thread=False, time=False + ) class BaseDbTestCase(BaseTestCase): @@ -360,7 +356,7 @@ def _wait_on_status( assert isinstance(status, six.string_types), "%s is not of text type" % (status) for _ in range(0, retries): - eventlet.sleep(delay) + concurrency.sleep(delay) liveaction_db = LiveAction.get_by_id(str(liveaction_db.id)) if liveaction_db.status == status: break @@ -378,7 +374,7 @@ def _wait_on_statuses( ) for _ in range(0, retries): - eventlet.sleep(delay) + concurrency.sleep(delay) liveaction_db = LiveAction.get_by_id(str(liveaction_db.id)) if liveaction_db.status in statuses: break @@ -392,7 +388,7 @@ def _wait_on_ac_ex_status( self, execution_db, status, retries=300, delay=0.1, raise_exc=True ): for _ in range(0, retries): - eventlet.sleep(delay) + concurrency.sleep(delay) execution_db = ex_db_access.ActionExecution.get_by_id(str(execution_db.id)) if execution_db.status == status: break @@ -406,7 +402,7 @@ def _wait_on_call_count( self, mocked, expected_count, retries=100, delay=0.1, raise_exc=True ): for _ in range(0, retries): - eventlet.sleep(delay) + concurrency.sleep(delay) if mocked.call_count == expected_count: break @@ -831,7 +827,7 @@ def make_mock_stream_readline(mock_stream, mock_data, stop_counter=1, sleep_dela def mock_stream_readline(): if sleep_delay: - eventlet.sleep(sleep_delay) + concurrency.sleep(sleep_delay) if mock_stream.counter >= stop_counter: mock_stream.closed = True diff --git a/st2tests/st2tests/mocks/execution.py b/st2tests/st2tests/mocks/execution.py index 00e3c8ef11..d54defeb19 100644 --- a/st2tests/st2tests/mocks/execution.py +++ b/st2tests/st2tests/mocks/execution.py @@ -14,9 +14,10 @@ # limitations under the License. from __future__ import absolute_import -import eventlet import traceback +from st2common.util import concurrency + from st2actions.notifier import notifier from st2common.models.db.execution import ActionExecutionDB @@ -40,7 +41,7 @@ class MockExecutionPublisherNonBlocking(object): def publish_update(cls, payload): try: if isinstance(payload, ActionExecutionDB): - eventlet.spawn(notifier.get_notifier().process, payload) + concurrency.spawn(notifier.get_notifier().process, payload) except Exception: traceback.print_exc() print(payload) diff --git a/st2tests/st2tests/mocks/liveaction.py b/st2tests/st2tests/mocks/liveaction.py index 2b329e6b25..3a7a696d79 100644 --- a/st2tests/st2tests/mocks/liveaction.py +++ b/st2tests/st2tests/mocks/liveaction.py @@ -17,9 +17,10 @@ from __future__ import print_function import six -import eventlet import traceback +from st2common.util import concurrency + from st2actions import worker from st2actions.scheduler import entrypoint as scheduling from st2actions.scheduler import handler as scheduling_queue @@ -79,10 +80,10 @@ def publish_state(cls, payload, state): try: if isinstance(payload, LiveActionDB): if state == action_constants.LIVEACTION_STATUS_REQUESTED: - thread = eventlet.spawn(cls.process, payload) + thread = concurrency.spawn(cls.process, payload) cls.threads.append(thread) else: - thread = eventlet.spawn(worker.get_worker().process, payload) + thread = concurrency.spawn(worker.get_worker().process, payload) cls.threads.append(thread) except Exception: traceback.print_exc() @@ -92,13 +93,13 @@ def publish_state(cls, payload, state): def wait_all(cls): for thread in cls.threads: try: - thread.wait() + concurrency.wait(thread) except Exception as e: print(six.text_type(e)) finally: cls.threads.remove(thread) - eventlet.sleep(0.1) + concurrency.sleep(0.1) class MockLiveActionPublisherSchedulingQueueOnly(object): diff --git a/st2tests/st2tests/mocks/workflow.py b/st2tests/st2tests/mocks/workflow.py index 051bf5cb83..f9c8180df2 100644 --- a/st2tests/st2tests/mocks/workflow.py +++ b/st2tests/st2tests/mocks/workflow.py @@ -16,9 +16,10 @@ from __future__ import absolute_import import six -import eventlet import traceback +from st2common.util import concurrency + from st2actions.workflows import workflows from st2common.models.db import workflow as wf_ex_db @@ -53,7 +54,7 @@ class MockWorkflowExecutionPublisherNonBlocking(object): def publish_create(cls, payload): try: if isinstance(payload, wf_ex_db.WorkflowExecutionDB): - thread = eventlet.spawn(workflows.get_engine().process, payload) + thread = concurrency.spawn(workflows.get_engine().process, payload) cls.threads.append(thread) except Exception: traceback.print_exc() @@ -63,7 +64,7 @@ def publish_create(cls, payload): def publish_state(cls, payload, state): try: if isinstance(payload, wf_ex_db.WorkflowExecutionDB): - thread = eventlet.spawn(workflows.get_engine().process, payload) + thread = concurrency.spawn(workflows.get_engine().process, payload) cls.threads.append(thread) except Exception: traceback.print_exc() @@ -73,7 +74,7 @@ def publish_state(cls, payload, state): def wait_all(cls): for thread in cls.threads: try: - thread.wait() + concurrency.wait(thread) except Exception as e: print(six.text_type(e)) finally: