Skip to content

Commit 461e7ac

Browse files
rm-youralonsoh
authored andcommitted
Fix unbounded thread pool in OVN MaintenanceThread
Change-Id Ie01fd776 (commit d98d028) switched MaintenanceThread's PeriodicWorker from the default SynchronousExecutor to futurist.ThreadPoolExecutor, passing the class as a bare reference without specifying max_workers. When max_workers is unset, futurist.ThreadPoolExecutor defaults to os.cpu_count() * 5 (480 on a 96-CPU node). The executor's _maybe_spin_up() creates a new idle worker thread on every submit() call, and futurist.ThreadPoolExecutor never shrinks its pool. Threads accumulate indefinitely until RuntimeError: can't start new thread. Since PeriodicWorker runs periodic tasks sequentially (one at a time), only a single worker thread is needed. Bound max_workers=1 to prevent thread accumulation. Closes-Bug: #2149830 Related-Bug: #2097257 Generated-By: claude-opus-4-6 (OpenCode) Signed-off-by: Adam Harwell <flux.adam@gmail.com> Change-Id: Id0b71902a7dc842f2190b310a222f49f39db91ba (cherry picked from commit 153db8a)
1 parent 6f49b3b commit 461e7ac

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def start(self):
104104
if self._thread is None:
105105
self._worker = periodics.PeriodicWorker(
106106
self._callables,
107-
executor_factory=futurist.ThreadPoolExecutor)
107+
executor_factory=lambda: futurist.ThreadPoolExecutor(
108+
max_workers=1))
108109
self._thread = threading.Thread(target=self._worker.start)
109110
self._thread.daemon = True
110111
self._thread.start()

neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# License for the specific language governing permissions and limitations
1414
# under the License.
1515

16+
import threading
17+
import time
1618
from unittest import mock
1719

1820
from futurist import periodics
@@ -35,6 +37,49 @@
3537
from neutron.tests.unit import testlib_api
3638

3739

40+
class TestMaintenanceThread(base.BaseTestCase):
41+
42+
def test_start_bounds_thread_pool_executor(self):
43+
"""Verify MaintenanceThread runs periodic tasks with one worker."""
44+
num_tasks = 5
45+
run_counter = [0] * num_tasks
46+
active_threads = []
47+
target_runs = 5
48+
49+
class _PeriodicTask:
50+
def __init__(self, idx):
51+
self.idx = idx
52+
53+
@periodics.periodic(spacing=0.05)
54+
def trivial_task(self):
55+
run_counter[self.idx] += 1
56+
active_threads.append(threading.active_count())
57+
time.sleep(0)
58+
if run_counter[self.idx] >= target_runs:
59+
raise periodics.NeverAgain
60+
61+
mt = maintenance.MaintenanceThread()
62+
for idx in range(num_tasks):
63+
mt.add_periodics(_PeriodicTask(idx))
64+
65+
threads_before = threading.active_count()
66+
mt.start()
67+
threads_after_start = threading.active_count()
68+
self.assertGreater(threads_after_start, threads_before)
69+
70+
executor = mt._worker._executor_factory()
71+
# Let some tasks be executed.
72+
time.sleep(2)
73+
threads_after_wait = threading.active_count()
74+
self.assertEqual(threads_after_wait, threads_after_start + 1)
75+
76+
executor.shutdown(wait=False)
77+
mt._thread.join(timeout=3)
78+
threads_end = threading.active_count()
79+
self.assertEqual(threads_end, threads_after_wait)
80+
self.assertTrue(all(target_runs == _c for _c in run_counter))
81+
82+
3883
class TestHasLockPeriodicDecorator(base.BaseTestCase):
3984

4085
def test_decorator_no_limit_have_lock(self):

0 commit comments

Comments
 (0)