personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_local_admission.py
15 kB 511 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import asyncio 7import errno 8import json 9import os 10import subprocess 11import sys 12import threading 13import time 14 15import pytest 16 17from solstone.think.providers.local_admission import ( 18 LocalAdmissionTimeout, 19 LocalSlotLease, 20 acquire_local_slot, 21 acquire_local_slot_async, 22 record_local_inference, 23) 24 25 26def _isolated_journal(monkeypatch, tmp_path): 27 import solstone.think.utils as think_utils 28 29 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 30 think_utils._journal_path_cache = None 31 32 33def _admission_root(tmp_path): 34 return tmp_path / "health" / "local-inference-admission" 35 36 37def _wait_for(predicate, timeout_s: float = 1.0) -> None: 38 deadline = time.monotonic() + timeout_s 39 while not predicate(): 40 if time.monotonic() >= deadline: 41 raise AssertionError("condition was not met before timeout") 42 time.sleep(0.005) 43 44 45def _wait_for_ticket_count(tmp_path, count: int) -> None: 46 root = _admission_root(tmp_path) 47 _wait_for(lambda: len(list(root.glob("wait-*.ticket"))) >= count) 48 49 50def test_cross_thread_admission_never_exceeds_capacity(monkeypatch, tmp_path): 51 _isolated_journal(monkeypatch, tmp_path) 52 active = 0 53 peak = 0 54 lock = threading.Lock() 55 56 def work() -> None: 57 nonlocal active, peak 58 with acquire_local_slot(2, 2): 59 with lock: 60 active += 1 61 peak = max(peak, active) 62 time.sleep(0.04) 63 with lock: 64 active -= 1 65 66 threads = [threading.Thread(target=work) for _ in range(8)] 67 for thread in threads: 68 thread.start() 69 for thread in threads: 70 thread.join() 71 72 assert peak == 2 73 74 75def test_failure_releases_permit(monkeypatch, tmp_path): 76 _isolated_journal(monkeypatch, tmp_path) 77 78 with pytest.raises(RuntimeError, match="boom"): 79 with acquire_local_slot(1, 0.5): 80 raise RuntimeError("boom") 81 82 with acquire_local_slot(1, 0.1) as permit: 83 assert permit.slot_index == 0 84 85 86def test_sync_queue_timeout(monkeypatch, tmp_path): 87 _isolated_journal(monkeypatch, tmp_path) 88 first = acquire_local_slot(1, 0.1) 89 try: 90 with pytest.raises(LocalAdmissionTimeout): 91 acquire_local_slot(1, 0.03) 92 finally: 93 first.release() 94 95 96def test_exclusive_waits_for_single_slot_holder(monkeypatch, tmp_path): 97 _isolated_journal(monkeypatch, tmp_path) 98 holder = acquire_local_slot(2, 0.1) 99 acquired: list[int] = [] 100 101 def wait_exclusive() -> None: 102 with acquire_local_slot(2, 1.0, exclusive=True) as permit: 103 acquired.append(permit.slot_index) 104 105 thread = threading.Thread(target=wait_exclusive) 106 thread.start() 107 time.sleep(0.05) 108 assert acquired == [] 109 110 holder.release() 111 thread.join(timeout=1.0) 112 113 assert not thread.is_alive() 114 assert acquired == [0] 115 116 117def test_exclusive_capacity_one_degenerates_to_single_slot(monkeypatch, tmp_path): 118 _isolated_journal(monkeypatch, tmp_path) 119 120 with acquire_local_slot(1, 0.1, exclusive=True) as permit: 121 assert permit.slot_index == 0 122 assert permit.capacity == 1 123 124 125def test_exclusive_timeout_does_not_strand_partial_locks(monkeypatch, tmp_path): 126 _isolated_journal(monkeypatch, tmp_path) 127 slot_zero = acquire_local_slot(2, 0.1) 128 slot_one = acquire_local_slot(2, 0.1) 129 slot_zero.release() 130 try: 131 with pytest.raises(LocalAdmissionTimeout): 132 acquire_local_slot(2, 0.03, exclusive=True) 133 134 with acquire_local_slot(2, 0.1) as permit: 135 assert permit.slot_index == 0 136 finally: 137 slot_one.release() 138 139 140def test_exclusive_flock_error_releases_partial_slot_set(monkeypatch, tmp_path): 141 _isolated_journal(monkeypatch, tmp_path) 142 143 from solstone.think.providers import local_admission 144 145 real_flock = local_admission.fcntl.flock 146 147 def fail_second_slot(lock_file, operation): 148 if ( 149 str(lock_file.name).endswith("slot-1.lock") 150 and operation & local_admission.fcntl.LOCK_EX 151 and operation & local_admission.fcntl.LOCK_NB 152 ): 153 raise OSError(errno.EIO, "simulated flock failure") 154 return real_flock(lock_file, operation) 155 156 monkeypatch.setattr(local_admission.fcntl, "flock", fail_second_slot) 157 158 with pytest.raises(OSError): 159 acquire_local_slot(2, 0.1, exclusive=True) 160 161 with acquire_local_slot(1, 0.1) as permit: 162 assert permit.slot_index == 0 163 164 165def test_exclusive_failure_releases_all_slots(monkeypatch, tmp_path): 166 _isolated_journal(monkeypatch, tmp_path) 167 168 with pytest.raises(RuntimeError, match="boom"): 169 with acquire_local_slot(2, 0.5, exclusive=True): 170 raise RuntimeError("boom") 171 172 first = acquire_local_slot(2, 0.1) 173 try: 174 second = acquire_local_slot(2, 0.1) 175 try: 176 assert {first.slot_index, second.slot_index} == {0, 1} 177 finally: 178 second.release() 179 finally: 180 first.release() 181 182 183def test_async_queued_cancellation_does_not_leak(monkeypatch, tmp_path): 184 _isolated_journal(monkeypatch, tmp_path) 185 186 async def exercise() -> None: 187 first = await acquire_local_slot_async(1, 0.5) 188 queued = asyncio.create_task(acquire_local_slot_async(1, 1.0)) 189 await asyncio.sleep(0.05) 190 queued.cancel() 191 with pytest.raises(asyncio.CancelledError): 192 await queued 193 assert not list( 194 (tmp_path / "health" / "local-inference-admission").glob("wait-*.ticket") 195 ) 196 first.release() 197 second = await acquire_local_slot_async(1, 0.1) 198 second.release() 199 200 asyncio.run(exercise()) 201 202 203def test_sync_cancel_event_drops_ticket_and_releases_queue(monkeypatch, tmp_path): 204 _isolated_journal(monkeypatch, tmp_path) 205 206 from solstone.think.providers import local_admission 207 208 holder = acquire_local_slot(1, 0.1) 209 cancel_event = threading.Event() 210 errors: list[BaseException] = [] 211 212 def wait_for_slot() -> None: 213 try: 214 acquire_local_slot(1, 2.0, cancel_event=cancel_event) 215 except BaseException as exc: 216 errors.append(exc) 217 218 thread = threading.Thread(target=wait_for_slot) 219 thread.start() 220 _wait_for_ticket_count(tmp_path, 1) 221 222 cancel_event.set() 223 thread.join(timeout=1.0) 224 holder.release() 225 226 assert not thread.is_alive() 227 assert len(errors) == 1 228 assert isinstance(errors[0], local_admission.LocalAdmissionCancelled) 229 assert not list(_admission_root(tmp_path).glob("wait-*.ticket")) 230 with acquire_local_slot(1, 0.1) as permit: 231 assert permit.slot_index == 0 232 233 234def test_sync_cancel_event_after_acquire_releases_permit(monkeypatch, tmp_path): 235 _isolated_journal(monkeypatch, tmp_path) 236 237 from solstone.think.providers import local_admission 238 239 cancel_event = threading.Event() 240 real_try_acquire = local_admission._try_acquire 241 242 def cancel_after_acquire(capacity, started, root): 243 permit = real_try_acquire(capacity, started, root) 244 if permit is not None: 245 cancel_event.set() 246 return permit 247 248 monkeypatch.setattr(local_admission, "_try_acquire", cancel_after_acquire) 249 250 with pytest.raises(local_admission.LocalAdmissionCancelled): 251 acquire_local_slot(1, 0.1, cancel_event=cancel_event) 252 253 monkeypatch.setattr(local_admission, "_try_acquire", real_try_acquire) 254 with acquire_local_slot(1, 0.1) as permit: 255 assert permit.slot_index == 0 256 257 258def test_lease_close_cancels_pending_reacquire_without_leaking( 259 monkeypatch, 260 tmp_path, 261): 262 _isolated_journal(monkeypatch, tmp_path) 263 264 from solstone.think.providers import local_admission 265 266 initial = acquire_local_slot(1, 0.1) 267 lease = LocalSlotLease( 268 capacity=1, 269 deadline=time.monotonic() + 2.0, 270 permit=initial, 271 ) 272 lease.yield_slot() 273 holder = acquire_local_slot(1, 0.1) 274 errors: list[BaseException] = [] 275 276 def reacquire() -> None: 277 try: 278 lease.reacquire() 279 except BaseException as exc: 280 errors.append(exc) 281 282 thread = threading.Thread(target=reacquire) 283 thread.start() 284 _wait_for_ticket_count(tmp_path, 1) 285 286 lease.close() 287 thread.join(timeout=1.0) 288 holder.release() 289 290 assert not thread.is_alive() 291 assert len(errors) == 1 292 assert isinstance(errors[0], local_admission.LocalAdmissionCancelled) 293 assert not list(_admission_root(tmp_path).glob("wait-*.ticket")) 294 with acquire_local_slot(1, 0.1) as permit: 295 assert permit.slot_index == 0 296 297 298def test_waiters_are_admitted_in_ticket_order(monkeypatch, tmp_path): 299 _isolated_journal(monkeypatch, tmp_path) 300 root = _admission_root(tmp_path) 301 first = acquire_local_slot(1, 1) 302 order: list[int] = [] 303 304 def wait(index: int) -> None: 305 with acquire_local_slot(1, 2): 306 order.append(index) 307 time.sleep(0.01) 308 309 threads = [] 310 for index in range(5): 311 thread = threading.Thread(target=wait, args=(index,)) 312 thread.start() 313 threads.append(thread) 314 deadline = time.monotonic() + 1 315 while len(list(root.glob("wait-*.ticket"))) < index + 1: 316 assert time.monotonic() < deadline 317 time.sleep(0.005) 318 319 first.release() 320 for thread in threads: 321 thread.join() 322 323 assert order == list(range(5)) 324 325 326def test_yielded_parent_reacquire_respects_existing_fifo_waiter( 327 monkeypatch, 328 tmp_path, 329): 330 _isolated_journal(monkeypatch, tmp_path) 331 332 initial = acquire_local_slot(1, 0.1) 333 lease = LocalSlotLease( 334 capacity=1, 335 deadline=time.monotonic() + 2.0, 336 permit=initial, 337 ) 338 order: list[str] = [] 339 waiter_entered = threading.Event() 340 release_waiter = threading.Event() 341 parent_reacquired = threading.Event() 342 343 def waiter() -> None: 344 with acquire_local_slot(1, 2.0): 345 order.append("waiter") 346 waiter_entered.set() 347 assert release_waiter.wait(1.0) 348 349 waiter_thread = threading.Thread(target=waiter) 350 waiter_thread.start() 351 _wait_for_ticket_count(tmp_path, 1) 352 353 lease.yield_slot() 354 assert waiter_entered.wait(1.0) 355 356 def parent() -> None: 357 lease.reacquire() 358 order.append("parent") 359 parent_reacquired.set() 360 361 parent_thread = threading.Thread(target=parent) 362 parent_thread.start() 363 _wait_for_ticket_count(tmp_path, 1) 364 365 assert order == ["waiter"] 366 release_waiter.set() 367 assert parent_reacquired.wait(1.0) 368 parent_thread.join(timeout=1.0) 369 waiter_thread.join(timeout=1.0) 370 lease.close() 371 372 assert not parent_thread.is_alive() 373 assert not waiter_thread.is_alive() 374 assert order == ["waiter", "parent"] 375 376 377def test_cross_process_nested_acquire_succeeds_while_parent_yielded( 378 monkeypatch, 379 tmp_path, 380): 381 _isolated_journal(monkeypatch, tmp_path) 382 383 initial = acquire_local_slot(1, 0.1) 384 lease = LocalSlotLease( 385 capacity=1, 386 deadline=time.monotonic() + 2.0, 387 permit=initial, 388 ) 389 env = {**os.environ, "SOLSTONE_JOURNAL": str(tmp_path)} 390 script = """ 391from solstone.think.providers.local_admission import acquire_local_slot 392with acquire_local_slot(1, 1.0) as permit: 393 assert permit.slot_index == 0 394""" 395 396 try: 397 lease.yield_slot() 398 completed = subprocess.run( 399 [sys.executable, "-c", script], 400 env=env, 401 text=True, 402 capture_output=True, 403 timeout=2.0, 404 check=False, 405 ) 406 assert completed.returncode == 0, completed.stderr 407 lease.reacquire() 408 finally: 409 lease.close() 410 411 with acquire_local_slot(1, 0.1) as permit: 412 assert permit.slot_index == 0 413 414 415def test_capacity_two_two_parents_run_cross_process_nested_acquires( 416 monkeypatch, 417 tmp_path, 418): 419 _isolated_journal(monkeypatch, tmp_path) 420 421 env = {**os.environ, "SOLSTONE_JOURNAL": str(tmp_path)} 422 script = """ 423from solstone.think.providers.local_admission import acquire_local_slot 424with acquire_local_slot(2, 1.0): 425 pass 426""" 427 barrier = threading.Barrier(2) 428 results: list[int] = [] 429 errors: list[BaseException] = [] 430 lock = threading.Lock() 431 432 def parent() -> None: 433 lease: LocalSlotLease | None = None 434 try: 435 permit = acquire_local_slot(2, 1.0) 436 lease = LocalSlotLease( 437 capacity=2, 438 deadline=time.monotonic() + 3.0, 439 permit=permit, 440 ) 441 barrier.wait(timeout=1.0) 442 lease.yield_slot() 443 completed = subprocess.run( 444 [sys.executable, "-c", script], 445 env=env, 446 text=True, 447 capture_output=True, 448 timeout=2.0, 449 check=False, 450 ) 451 with lock: 452 results.append(completed.returncode) 453 if completed.returncode != 0: 454 raise AssertionError(completed.stderr) 455 lease.reacquire() 456 except BaseException as exc: 457 with lock: 458 errors.append(exc) 459 finally: 460 if lease is not None: 461 lease.close() 462 463 threads = [threading.Thread(target=parent) for _ in range(2)] 464 for thread in threads: 465 thread.start() 466 for thread in threads: 467 thread.join(timeout=4.0) 468 469 assert all(not thread.is_alive() for thread in threads) 470 assert errors == [] 471 assert results == [0, 0] 472 first = acquire_local_slot(2, 0.1) 473 try: 474 second = acquire_local_slot(2, 0.1) 475 try: 476 assert {first.slot_index, second.slot_index} == {0, 1} 477 finally: 478 second.release() 479 finally: 480 first.release() 481 482 483def test_stale_ticket_from_exited_owner_is_pruned(monkeypatch, tmp_path): 484 _isolated_journal(monkeypatch, tmp_path) 485 root = tmp_path / "health" / "local-inference-admission" 486 root.mkdir(parents=True) 487 stale = root / "wait-00000000000000000000-1-stale.ticket" 488 stale.write_text("", encoding="utf-8") 489 490 with acquire_local_slot(1, 0.2): 491 assert not stale.exists() 492 493 494def test_telemetry_is_durable_and_content_free(monkeypatch, tmp_path): 495 _isolated_journal(monkeypatch, tmp_path) 496 record_local_inference( 497 { 498 "timestamp": 1.0, 499 "request_id": "abc", 500 "provider": "local", 501 "model": "local/qwen3.5-4b", 502 "queue_wait_ms": 12.5, 503 "outcome": "success", 504 } 505 ) 506 507 path = tmp_path / "health" / "local-inference" / time.strftime("%Y%m%d.jsonl") 508 row = json.loads(path.read_text(encoding="utf-8")) 509 assert row["request_id"] == "abc" 510 assert "prompt" not in row 511 assert "output" not in row