127 lines
3.7 KiB
Python
127 lines
3.7 KiB
Python
import threading
|
|
import time
|
|
import unittest
|
|
|
|
from bulkhead import Bulkhead, BulkheadFull
|
|
|
|
|
|
class TestBulkheadBasic(unittest.TestCase):
|
|
def test_invalid_args(self):
|
|
with self.assertRaises(ValueError):
|
|
Bulkhead(0)
|
|
with self.assertRaises(ValueError):
|
|
Bulkhead(-1)
|
|
with self.assertRaises(ValueError):
|
|
Bulkhead(1, -1)
|
|
with self.assertRaises(ValueError):
|
|
Bulkhead(1, max_queue=-1)
|
|
|
|
def test_try_acquire_fast_path(self):
|
|
bh = Bulkhead(2)
|
|
h1 = bh.try_acquire()
|
|
h2 = bh.try_acquire()
|
|
self.assertIsNotNone(h1)
|
|
self.assertIsNotNone(h2)
|
|
self.assertIsNone(bh.try_acquire())
|
|
self.assertEqual(bh.active(), 2)
|
|
h1.release()
|
|
h2.release()
|
|
self.assertEqual(bh.active(), 0)
|
|
|
|
def test_acquire_release_context(self):
|
|
bh = Bulkhead(1)
|
|
with bh.slot():
|
|
self.assertEqual(bh.active(), 1)
|
|
self.assertIsNone(bh.try_acquire())
|
|
self.assertEqual(bh.active(), 0)
|
|
|
|
def test_run(self):
|
|
bh = Bulkhead(1)
|
|
self.assertEqual(bh.run(lambda x: x + 1, 4), 5)
|
|
|
|
def test_queue_full_raises(self):
|
|
bh = Bulkhead(1, max_queue=0)
|
|
h = bh.acquire()
|
|
with self.assertRaises(BulkheadFull):
|
|
bh.acquire()
|
|
h.release()
|
|
|
|
def test_queued_counter(self):
|
|
bh = Bulkhead(1, max_queue=2)
|
|
h = bh.acquire()
|
|
# two waiters queue
|
|
results = []
|
|
def worker(i):
|
|
try:
|
|
hh = bh.acquire(timeout=5)
|
|
results.append(("ok", i))
|
|
time.sleep(0.2)
|
|
hh.release()
|
|
except Exception as e:
|
|
results.append(("err", i, type(e).__name__))
|
|
ts = [threading.Thread(target=worker, args=(i,)) for i in range(2)]
|
|
for t in ts: t.start()
|
|
time.sleep(0.1)
|
|
self.assertEqual(bh.queued(), 2)
|
|
h.release()
|
|
for t in ts: t.join(timeout=5)
|
|
self.assertEqual(bh.active(), 0)
|
|
self.assertEqual(len([r for r in results if r[0] == "ok"]), 2)
|
|
|
|
def test_timeout(self):
|
|
bh = Bulkhead(1, max_queue=1)
|
|
h = bh.acquire()
|
|
t0 = time.monotonic()
|
|
with self.assertRaises(TimeoutError):
|
|
bh.acquire(timeout=0.1)
|
|
elapsed = time.monotonic() - t0
|
|
self.assertLess(elapsed, 1.0)
|
|
# queued counter restored
|
|
self.assertEqual(bh.queued(), 0)
|
|
h.release()
|
|
|
|
def test_timeout_negative(self):
|
|
bh = Bulkhead(1)
|
|
with self.assertRaises(ValueError):
|
|
bh.acquire(timeout=-1)
|
|
|
|
def test_double_release_idempotent(self):
|
|
bh = Bulkhead(1)
|
|
h = bh.acquire()
|
|
h.release()
|
|
h.release()
|
|
self.assertEqual(bh.active(), 0)
|
|
# slot reusable
|
|
h2 = bh.try_acquire()
|
|
self.assertIsNotNone(h2)
|
|
h2.release()
|
|
|
|
def test_concurrent_stress(self):
|
|
bh = Bulkhead(4, max_queue=100)
|
|
active_peak = [0]
|
|
lock = threading.Lock()
|
|
def work():
|
|
with bh.slot():
|
|
with lock:
|
|
# approximate peak via active()
|
|
pass
|
|
time.sleep(0.01)
|
|
ts = [threading.Thread(target=work) for _ in range(50)]
|
|
for t in ts: t.start()
|
|
for t in ts: t.join(timeout=10)
|
|
self.assertEqual(bh.active(), 0)
|
|
self.assertEqual(bh.queued(), 0)
|
|
|
|
def test_handle_context_exit_releases_on_exception(self):
|
|
bh = Bulkhead(1)
|
|
try:
|
|
with bh.slot():
|
|
raise ValueError("boom")
|
|
except ValueError:
|
|
pass
|
|
self.assertEqual(bh.active(), 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|