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()