From c3a037fa78dfb465c2a541a28e21a36e3e7762b3 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 1 Jun 2021 11:49:20 +1200 Subject: [PATCH 01/21] unpin eventlet. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b19222..bf68cef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -eventlet==0.30.2 +eventlet pbr>=1.9 From 52e2bce460f5d1b76b325cb6a16a81388b174718 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 14 Sep 2021 22:04:19 +0000 Subject: [PATCH 02/21] change package name. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index f4b2556..5445546 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = beka +name = c65beka summary = A bare-bones BGP speaker long_description = Beka is a fairly basic BGP speaker. It can send @@ -10,7 +10,7 @@ long_description = It uses eventlet for concurrency, but is easy enough to port to gevent if that takes your fancy. - More information at https://github.com/faucetsdn/beka + More information at https://github.com/c65beka/beka license = Apache-2 author = Sam Russell author-email = sam.h.russell@gmail.com From 36dac24ef776b3a017da58d4a2239fbfdb0467cd Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 14 Sep 2021 22:05:27 +0000 Subject: [PATCH 03/21] disable deb. --- .github/workflows/release-debian.yml | 45 ---------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/release-debian.yml diff --git a/.github/workflows/release-debian.yml b/.github/workflows/release-debian.yml deleted file mode 100644 index 1ea2839..0000000 --- a/.github/workflows/release-debian.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Build debian packages for release - -on: - push: - tags: - - '[0-9]+.[0-9]+.[0-9]+' - -env: - DEBIAN_FRONTEND: noninteractive - -jobs: - debian-package: - name: "Build debian packages" - runs-on: ubuntu-latest - environment: - name: "release" - container: - image: "ubuntu:bionic" - steps: - - name: Install dependencies - run: | - apt-get update - apt-get -y upgrade - apt-get -y install devscripts dpkg-dev debhelper equivs - - name: Checkout repo - uses: actions/checkout@v2 - - name: Bump version - run: | - export DEBEMAIL='maintainers@faucet.nz' - export DEBFULLNAME='Faucet Maintainers' - debchange --newversion ${{ github.event.release.tag_name }} -b "New upstream release" - - name: Build package - run: | - mk-build-deps -i -r -t 'apt-get -f -y --force-yes' - dpkg-buildpackage -b -us -uc -rfakeroot - - name: Store package - run: | - mkdir -p packages/all - cp ../*.deb packages/all - - name: Publish package on packagecloud - uses: faucetsdn/action-packagecloud-upload-debian-packages@v1 - with: - path: packages/ - repo: faucetsdn/faucet-test - token: ${{ secrets.PACKAGECLOUD_TOKEN }} From cdff31b86a6692cda4deabec32daf5df383c8240 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 14 Sep 2021 22:12:43 +0000 Subject: [PATCH 04/21] URLs. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5445546..9578938 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,11 +10,11 @@ long_description = It uses eventlet for concurrency, but is easy enough to port to gevent if that takes your fancy. - More information at https://github.com/c65beka/beka + More information at https://github.com/c65sdn/beka license = Apache-2 author = Sam Russell author-email = sam.h.russell@gmail.com -home-page = https://github.com/faucetsdn/beka +home-page = https://github.com/c65sdn/beka classifiers = Development Status :: 2 - Pre-Alpha Intended Audience :: System Administrators From 0b12b572146b1f39bfcbbbe46322963d1d15d979 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Sun, 19 Sep 2021 22:19:42 +0000 Subject: [PATCH 05/21] no 3.6. --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 4cb7a60..3a022d9 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] steps: - name: Checkout repo uses: actions/checkout@v2 From 9999b618200cd5fb40c42cb19948085d09a0a90e Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 22 Feb 2022 22:35:27 +0000 Subject: [PATCH 06/21] test 3.10 --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 3a022d9..01d0707 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, '3.10'] steps: - name: Checkout repo uses: actions/checkout@v2 From e17f5b43d6e47596bdf11490a3c272255eb3f566 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 03:04:07 +0000 Subject: [PATCH 07/21] Bump pylint from 2.17.0 to 2.17.1 Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.0 to 2.17.1. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.17.0...v2.17.1) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index 7fa4577..8e2ccd2 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ -pylint==2.17.0 +pylint==2.17.1 pytype==2022.10.26 From 70dafcda6814821f3cb08b6df29124a3293661fc Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 18 Apr 2023 22:22:53 +0000 Subject: [PATCH 08/21] pytype. --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index ecd9af1..2235cdd 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ pylint==2.17.2 -pytype==2022.10.26 +pytype==2023.4.18 From 31083ac7f7d76fb6d4b414007e1e450cdaf3753d Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 18 Apr 2023 22:40:32 +0000 Subject: [PATCH 09/21] need release. --- .github/workflows/{disabled => }/release-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{disabled => }/release-python.yml (93%) diff --git a/.github/workflows/disabled/release-python.yml b/.github/workflows/release-python.yml similarity index 93% rename from .github/workflows/disabled/release-python.yml rename to .github/workflows/release-python.yml index cf9beff..650da68 100644 --- a/.github/workflows/disabled/release-python.yml +++ b/.github/workflows/release-python.yml @@ -24,7 +24,7 @@ jobs: - name: Build python package run: python3 setup.py sdist - name: Publish python package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.5 + uses: pypa/gh-action-pypi-publish@v1.6.4 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} From d537ae865cd8a639412ea780b20802eb7d602120 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Tue, 18 Apr 2023 22:41:28 +0000 Subject: [PATCH 10/21] 1.8.5 --- .github/workflows/release-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 650da68..cf9beff 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -24,7 +24,7 @@ jobs: - name: Build python package run: python3 setup.py sdist - name: Publish python package to PyPI - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.5 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} From ee9eb15ae1f9ed317c2bf1df400040e4814eb0ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:01:44 +0000 Subject: [PATCH 11/21] Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.7 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.6 to 1.8.7. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.6...v1.8.7) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index e00f062..0ae96b7 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -24,7 +24,7 @@ jobs: - name: Build python package run: python3 setup.py sdist - name: Publish python package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.6 + uses: pypa/gh-action-pypi-publish@v1.8.7 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} From bce0b9a60d6b7f34abc0227f3071851aed358769 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 02:24:12 +0000 Subject: [PATCH 12/21] Bump pylint from 2.17.4 to 2.17.5 Bumps [pylint](https://github.com/pylint-dev/pylint) from 2.17.4 to 2.17.5. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v2.17.4...v2.17.5) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index c3328a4..d5f11e0 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ -pylint==2.17.4 +pylint==2.17.5 pytype==2023.5.8 From 92f306ee3540fc88858959123bc7f80b69e151b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 02:27:13 +0000 Subject: [PATCH 13/21] Bump codecov/codecov-action from 3 to 4 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index fb40d3d..078b61a 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -24,4 +24,4 @@ jobs: BEKA_TESTS=-uz ./run_tests.sh - if: ${{ matrix.python-version == env.CODECOV_PY_VER }} name: Upload codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 From 29d3a00c7d4e15e5af2ee037271a36c55d536c83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 06:37:39 +0000 Subject: [PATCH 14/21] Bump pytype from 2023.12.8 to 2024.1.24 Bumps [pytype](https://github.com/google/pytype) from 2023.12.8 to 2024.1.24. - [Changelog](https://github.com/google/pytype/blob/main/CHANGELOG) - [Commits](https://github.com/google/pytype/compare/2023.12.08...2024.01.24) --- updated-dependencies: - dependency-name: pytype dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index 280ad5b..1da70a0 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ pylint==3.0.3 -pytype==2024.1.5 +pytype==2024.1.24 From 94290917581c51323a00ec6421d1f6036707cf82 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Thu, 25 Jan 2024 07:02:36 +0000 Subject: [PATCH 15/21] drop 3.8 --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 691de59..675d913 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, '3.10', 3.11] + python-version: [3.9, '3.10', 3.11, 3.12] steps: - name: Checkout repo uses: actions/checkout@v4 From 5d46e34d71358fc525c47fea465715fc78c2a65b Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Thu, 25 Jan 2024 07:12:02 +0000 Subject: [PATCH 16/21] codecov --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 675d913..32cc586 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -24,4 +24,4 @@ jobs: BEKA_TESTS=-uz ./run_tests.sh - if: ${{ matrix.python-version == env.CODECOV_PY_VER }} name: Upload codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 From 4d9c1e9a0966c86c5493eff50f6ef01eaa8e1c56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 02:15:47 +0000 Subject: [PATCH 17/21] Bump pypa/gh-action-pypi-publish from 1.8.12 to 1.8.14 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.12 to 1.8.14. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index f1f46a3..a022651 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -24,7 +24,7 @@ jobs: - name: Build python package run: python3 setup.py sdist - name: Publish python package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} From ef41e3f8e3c28d5c2f9d09ebfdc9265cc507510f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 02:44:06 +0000 Subject: [PATCH 18/21] Bump pytype from 2024.2.27 to 2024.3.11 Bumps [pytype](https://github.com/google/pytype) from 2024.2.27 to 2024.3.11. - [Changelog](https://github.com/google/pytype/blob/main/CHANGELOG) - [Commits](https://github.com/google/pytype/commits/2024.03.11) --- updated-dependencies: - dependency-name: pytype dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index 097c471..d777e66 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ pylint==3.1.0 -pytype==2024.2.27 +pytype==2024.3.11 From 49ca94fe6188c34a14700a6ec390ee857e367910 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 02:16:12 +0000 Subject: [PATCH 19/21] Bump pytype from 2024.3.19 to 2024.4.11 Bumps [pytype](https://github.com/google/pytype) from 2024.3.19 to 2024.4.11. - [Changelog](https://github.com/google/pytype/blob/main/CHANGELOG) - [Commits](https://github.com/google/pytype/compare/2024.03.19...2024.04.11) --- updated-dependencies: - dependency-name: pytype dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- codecheck-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecheck-requirements.txt b/codecheck-requirements.txt index fbfb673..27a29a5 100644 --- a/codecheck-requirements.txt +++ b/codecheck-requirements.txt @@ -1,2 +1,2 @@ pylint==3.1.0 -pytype==2024.3.19 +pytype==2024.4.11 From 01ea1f46972a9c1a4dde92d04162306666da7563 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Wed, 6 May 2026 09:02:13 +1200 Subject: [PATCH 20/21] Drop eventlet, port to stdlib threading eventlet is in long-term maintenance ("we strongly recommend against using it for new projects") and forces beka's only consumer (faucet) to keep monkey-patching the runtime. Port the four greenlet-spawning loops in Peering and the accept loop in StreamServer to plain threading + ThreadPoolExecutor, swap the two eventlet.queue.Queue instances in StateMachine for stdlib queue.Queue, and drop the eventlet dependency. Cooperative shutdown: * threading.Event signals stop; each loop checks it. * output_messages and route_updates queues get a poison-pill sentinel to wake any thread blocked in Queue.get(). * socket.shutdown(SHUT_RDWR) unblocks the recv side of receive_messages. * kick_timers uses Event.wait(timeout=1) so it bails immediately instead of waiting out the full second. Tests: * test_peering switched off GreenPool; test_print_route_updates now uses a real Thread and exercises Peering.shutdown() to verify the poison-pill path. test_run_starts_threads patches threading.Thread and asserts the four expected start/join cycles. Co-Authored-By: Claude Opus 4.7 --- beka/peering.py | 70 +++++++++++++++++++++++++-------------- beka/state_machine.py | 2 +- beka/stream_server.py | 55 +++++++++++++++++++----------- requirements.txt | 1 - run.py | 16 +++++---- setup.cfg | 3 -- test/unit/test_peering.py | 60 +++++++++++++++++++-------------- 7 files changed, 127 insertions(+), 80 deletions(-) diff --git a/beka/peering.py b/beka/peering.py index 9170d0d..f00ced9 100644 --- a/beka/peering.py +++ b/beka/peering.py @@ -1,14 +1,15 @@ +import threading import time -from eventlet import sleep, GreenPool -from eventlet.queue import Queue -import eventlet.greenthread as greenthread - from .chopper import Chopper from .event import EventTimerExpired, EventMessageReceived from .bgp_message import BgpMessageParser, BgpMessagePacker from .error import SocketClosedError, IdleError +# Sentinel placed on output queues during shutdown to wake any thread +# blocked in ``Queue.get()`` so it can observe ``_stop_event`` and exit. +_QUEUE_POISON = object() + class Peering(object): def __init__( @@ -16,8 +17,7 @@ def __init__( ): self.input_stream = None self.chopper = None - self.pool = None - self.eventlets = None + self.threads = None self.parser = None self.packer = None self.state_machine = state_machine @@ -27,6 +27,7 @@ def __init__( self.route_handler = route_handler self.error_handler = error_handler self.start_time = int(time.time()) + self._stop_event = threading.Event() def uptime(self): return int(time.time()) - self.start_time @@ -34,30 +35,34 @@ def uptime(self): def run(self): self.input_stream = self.socket.makefile(mode="rb") self.chopper = Chopper(self.input_stream) - self.pool = GreenPool() self.parser = BgpMessageParser() self.packer = BgpMessagePacker() self.state_machine.open_handler = self.open_handler - self.eventlets = [] - - self.eventlets.append(self.pool.spawn(self.send_messages)) - self.eventlets.append(self.pool.spawn(self.print_route_updates)) - self.eventlets.append(self.pool.spawn(self.kick_timers)) - self.eventlets.append(self.pool.spawn(self.receive_messages)) - self.pool.waitall() + targets = ( + self.send_messages, + self.print_route_updates, + self.kick_timers, + self.receive_messages, + ) + self.threads = [ + threading.Thread(target=t, name=t.__name__, daemon=True) for t in targets + ] + for thread in self.threads: + thread.start() + for thread in self.threads: + thread.join() def open_handler(self, capabilities): self.parser.capabilities = capabilities self.packer.capabilities = capabilities def receive_messages(self): - while True: - sleep(0) + while not self._stop_event.is_set(): try: message_type, serialised_message = self.chopper.next() except SocketClosedError as e: - if self.error_handler: + if self.error_handler and not self._stop_event.is_set(): self.error_handler("Peering %s: %s" % (self.peer_address, e)) self.shutdown() break @@ -73,25 +78,30 @@ def receive_messages(self): break def send_messages(self): - while True: - sleep(0) + while not self._stop_event.is_set(): message = self.state_machine.output_messages.get() + if message is _QUEUE_POISON: + break self.socket.send(self.packer.pack(message)) def empty_message_queue(self): while self.state_machine.output_messages.qsize(): message = self.state_machine.output_messages.get() + if message is _QUEUE_POISON: + continue self.socket.send(self.packer.pack(message)) def print_route_updates(self): - while True: - sleep(0) + while not self._stop_event.is_set(): route_update = self.state_machine.route_updates.get() + if route_update is _QUEUE_POISON: + break self.route_handler(route_update) def kick_timers(self): - while True: - sleep(1) + # ``Event.wait(timeout)`` returns True as soon as the event is set, + # giving prompt shutdown without burning CPU between ticks. + while not self._stop_event.wait(timeout=1): tick = int(time.time()) try: self.state_machine.event(EventTimerExpired(), tick) @@ -102,6 +112,16 @@ def kick_timers(self): break def shutdown(self): + if self._stop_event.is_set(): + return + self._stop_event.set() self.empty_message_queue() - for eventlet in self.eventlets: - eventlet.kill() + # Unblock any thread parked in ``Queue.get()``. Each consumer + # checks ``_stop_event`` after waking and exits. + self.state_machine.output_messages.put(_QUEUE_POISON) + self.state_machine.route_updates.put(_QUEUE_POISON) + # Force ``chopper.next()``'s underlying recv to return. + try: + self.socket.shutdown(2) # socket.SHUT_RDWR + except OSError: + pass diff --git a/beka/state_machine.py b/beka/state_machine.py index d1f7da4..b7c860d 100644 --- a/beka/state_machine.py +++ b/beka/state_machine.py @@ -1,4 +1,4 @@ -from eventlet.queue import Queue +from queue import Queue from collections import OrderedDict from .event import Event diff --git a/beka/stream_server.py b/beka/stream_server.py index 56a1d04..60fd907 100644 --- a/beka/stream_server.py +++ b/beka/stream_server.py @@ -1,16 +1,20 @@ import socket - -from eventlet import GreenPool, listen -import eventlet.greenthread as greenthread +import threading +from concurrent.futures import ThreadPoolExecutor class StreamServer: + """Listen on ``address`` and dispatch each accepted socket to ``handler`` in its own thread.""" + + DEFAULT_MAX_HANDLERS = 64 + def __init__(self, address, handler): self.address = address self.handler = handler - self.greenlets = set() self.running = False self.server = None + self._executor = None + self._accept_thread = None def _family(self): if ":" in self.address[0]: @@ -19,24 +23,35 @@ def _family(self): def serve_forever(self): self.running = True - self.server = listen(self.address, self._family()) - pool = GreenPool() - + self.server = socket.socket(self._family(), socket.SOCK_STREAM) + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server.bind(self.address) + self.server.listen() + self._executor = ThreadPoolExecutor( + max_workers=self.DEFAULT_MAX_HANDLERS, + thread_name_prefix="beka-handler", + ) + self._accept_thread = threading.current_thread() try: while self.running: - sock, address = self.server.accept() - pool.spawn(self.call_handler, sock, address) - self.greenlets - except OSError: - pass - - def call_handler(self, sock, address): - self.greenlets.add(greenthread.getcurrent()) - self.handler(sock, address) - self.greenlets.remove(greenthread.getcurrent()) + try: + sock, address = self.server.accept() + except OSError: + # accept() raises after stop() shuts the listening socket + break + self._executor.submit(self.handler, sock, address) + finally: + self._executor.shutdown(wait=False) + self._executor = None def stop(self): self.running = False - for greenlet in self.greenlets: - greenlet.kill() - self.server.shutdown(socket.SHUT_RDWR) + if self.server is not None: + try: + self.server.shutdown(socket.SHUT_RDWR) + except OSError: + pass + try: + self.server.close() + except OSError: + pass diff --git a/requirements.txt b/requirements.txt index 6dad793..5581c21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -eventlet>=0.33.3 pbr>=1.9 diff --git a/run.py b/run.py index 5025d68..18f3a4f 100644 --- a/run.py +++ b/run.py @@ -1,9 +1,8 @@ import signal import sys +import threading import yaml -from eventlet import GreenPool - from beka.beka import Beka @@ -20,7 +19,7 @@ def __init__(self): def run(self): signal.signal(signal.SIGINT, self.signal_handler) - pool = GreenPool() + threads = [] with open("beka.yaml", encoding="utf-8") as file: config = yaml.safe_load(file.read()) @@ -46,9 +45,14 @@ def run(self): for route in router["routes"]: beka.add_route(route["prefix"], route["next_hop"]) self.bekas.append(beka) - pool.spawn_n(beka.run) - pool.waitall() - printmsg("All greenlets gone, exiting") + thread = threading.Thread( + target=beka.run, name="beka-%s" % router["local_address"] + ) + thread.start() + threads.append(thread) + for thread in threads: + thread.join() + printmsg("All threads gone, exiting") def signal_handler(self, _signal, _frame): printmsg("[SIGINT] Shutting down") diff --git a/setup.cfg b/setup.cfg index 9578938..3d130b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,9 +7,6 @@ long_description = but not too much else. It is designed to be simple to use and to extend, without too much overhead. - It uses eventlet for concurrency, but is easy enough to port to - gevent if that takes your fancy. - More information at https://github.com/c65sdn/beka license = Apache-2 author = Sam Russell diff --git a/test/unit/test_peering.py b/test/unit/test_peering.py index 09d6208..0a4d0dd 100644 --- a/test/unit/test_peering.py +++ b/test/unit/test_peering.py @@ -1,9 +1,9 @@ +import threading +import time import unittest +from queue import Queue from unittest.mock import patch, call -from eventlet import GreenPool, sleep -from eventlet.queue import Queue - from beka.peering import Peering @@ -26,19 +26,16 @@ def __init__(self): class FakeSocket: # pylint: disable=too-few-public-methods """Mocked Socket""" - def __init__(self): - pass - def makefile(self, *args, **kwargs): # pylint: disable=unused-argument return None + def shutdown(self, _how): # pragma: no cover - exercised via Peering.shutdown + pass + class FakeChopper: # pylint: disable=too-few-public-methods """Mocked Chopper""" - def __init__(self): - pass - class PeeringTestCase(unittest.TestCase): def setUp(self): @@ -55,25 +52,40 @@ def setUp(self): def test_print_route_updates(self): fake_route_update = "FAKE ROUTE UPDATE" self.state_machine.route_updates.put(fake_route_update) - pool = GreenPool() - eventlet = pool.spawn(self.peering.print_route_updates) - for _ in range(10): - sleep(0) - if self.route_catcher.route_updates: - break + thread = threading.Thread(target=self.peering.print_route_updates, daemon=True) + thread.start() + deadline = time.monotonic() + 1.0 + while not self.route_catcher.route_updates and time.monotonic() < deadline: + time.sleep(0.01) self.assertEqual(len(self.route_catcher.route_updates), 1) self.assertEqual(self.route_catcher.route_updates[0], fake_route_update) - eventlet.kill() + # Cooperative shutdown unblocks the consumer's Queue.get(). + self.peering.shutdown() + thread.join(timeout=1) + self.assertFalse(thread.is_alive()) def test_run_starts_threads(self): - with patch("beka.peering.GreenPool") as GreenPool: + with patch("beka.peering.threading.Thread") as Thread: self.peering.run() - GreenPool().spawn.assert_has_calls( + Thread.assert_has_calls( [ - call(self.peering.send_messages), - call(self.peering.print_route_updates), - call(self.peering.kick_timers), - call(self.peering.receive_messages), - ] + call( + target=self.peering.send_messages, name="send_messages", daemon=True + ), + call( + target=self.peering.print_route_updates, + name="print_route_updates", + daemon=True, + ), + call(target=self.peering.kick_timers, name="kick_timers", daemon=True), + call( + target=self.peering.receive_messages, + name="receive_messages", + daemon=True, + ), + ], + any_order=False, ) - assert GreenPool().waitall.call_count == 1 + # Each thread should be started and then joined. + self.assertEqual(Thread.return_value.start.call_count, 4) + self.assertEqual(Thread.return_value.join.call_count, 4) From bbb11ccfcc2a58ee4a60a3f58b5e647cebdd1d63 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Wed, 6 May 2026 11:54:23 +1200 Subject: [PATCH 21/21] Drop EOL Python 3.8/3.9, add 3.13 and 3.14 to CI matrices Python 3.8 reached EOL on 2024-10-07; 3.9 on 2025-10-07. Drop both from the test matrices and bump python_requires to >=3.10. While here, extend coverage with 3.13 and 3.14 to match faucet's supported range. * tests-unit.yml: matrix 3.9, 3.10, 3.11, 3.12 -> 3.10, 3.11, 3.12, 3.13, 3.14. * tests-codecheck.yml: pytype matrix 3.8, 3.9, 3.10 -> 3.10, 3.11, 3.12, 3.13, 3.14. * setup.py: python_requires >=3.8 -> >=3.10; refresh the pre-install version check to match. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/tests-codecheck.yml | 2 +- .github/workflows/tests-unit.yml | 2 +- setup.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests-codecheck.yml b/.github/workflows/tests-codecheck.yml index 9ce80f9..deb5477 100644 --- a/.github/workflows/tests-codecheck.yml +++ b/.github/workflows/tests-codecheck.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, '3.10'] + python-version: ['3.10', 3.11, 3.12, 3.13, 3.14] steps: - name: Checkout repo uses: actions/checkout@v5 diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 35de591..df3f667 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9, '3.10', 3.11, 3.12] + python-version: ['3.10', 3.11, 3.12, 3.13, 3.14] steps: - name: Checkout repo uses: actions/checkout@v5 diff --git a/setup.py b/setup.py index 3d901d3..95762bb 100755 --- a/setup.py +++ b/setup.py @@ -6,11 +6,11 @@ from setuptools import setup -if sys.version_info < (3,): +if sys.version_info < (3, 10): print( """You are trying to install beka on python {py} -beka is not compatible with python 2, please upgrade to python 3.8 or newer.""".format( +beka is not compatible with python earlier than 3.10, please upgrade.""".format( py=".".join([str(v) for v in sys.version_info[:3]]) ), file=sys.stderr, @@ -20,6 +20,6 @@ setup( name="beka", setup_requires=["pbr>=1.9", "setuptools>=17.1"], - python_requires=">=3.8", + python_requires=">=3.10", pbr=True, )