Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -122,6 +122,13 @@ _GLOBAL_DEEPCASTLE_ENGINE = None
|
|
| 122 |
_ENGINE_LOCK = asyncio.Lock()
|
| 123 |
_ENGINE_IO_LOCK = asyncio.Lock()
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
def _engine_hash_mb() -> int:
|
| 127 |
try:
|
|
@@ -456,25 +463,35 @@ def normalize_search_stats(info: dict) -> Tuple[int, int, int]:
|
|
| 456 |
# βββ Bot Move (/move) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 457 |
@app.post("/move", response_model=MoveResponse)
|
| 458 |
async def get_move(request: MoveRequest):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
try:
|
| 460 |
engine = await get_deepcastle_engine()
|
| 461 |
board = chess.Board(request.fen)
|
| 462 |
limit = chess.engine.Limit(time=request.time, depth=request.depth)
|
| 463 |
tsec = _search_timeout_sec(request.time, request.depth)
|
| 464 |
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
score_cp, mate_in = get_normalized_score(info)
|
| 480 |
depth, nodes, nps = normalize_search_stats(info)
|
|
@@ -500,6 +517,11 @@ async def get_move(request: MoveRequest):
|
|
| 500 |
|
| 501 |
del result
|
| 502 |
del info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
return MoveResponse(
|
| 505 |
bestmove=best_move,
|
|
@@ -516,30 +538,42 @@ async def get_move(request: MoveRequest):
|
|
| 516 |
except Exception as e:
|
| 517 |
print(f"Error: {e}")
|
| 518 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
| 519 |
|
| 520 |
|
| 521 |
# βββ Hint Move (/analysis-move) βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 522 |
@app.post("/analysis-move", response_model=MoveResponse)
|
| 523 |
async def get_analysis_move(request: MoveRequest):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
try:
|
| 525 |
engine = await get_stockfish_engine()
|
| 526 |
board = chess.Board(request.fen)
|
| 527 |
limit = chess.engine.Limit(time=request.time, depth=request.depth)
|
| 528 |
tsec = _search_timeout_sec(request.time, request.depth)
|
| 529 |
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
score_cp, mate_in = get_normalized_score(info)
|
| 545 |
depth, nodes, nps = normalize_search_stats(info)
|
|
@@ -586,6 +620,8 @@ async def get_analysis_move(request: MoveRequest):
|
|
| 586 |
except Exception as e:
|
| 587 |
print(f"Analysis move error: {e}")
|
| 588 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
| 589 |
|
| 590 |
|
| 591 |
# βββ Openings DB βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -741,6 +777,16 @@ def get_move_classification(
|
|
| 741 |
# βββ Game Analysis (/analyze-game) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 742 |
@app.post("/analyze-game", response_model=AnalyzeResponse)
|
| 743 |
async def analyze_game(request: AnalyzeRequest):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
try:
|
| 745 |
engine = await get_stockfish_engine()
|
| 746 |
board = chess.Board(request.start_fen) if request.start_fen else chess.Board()
|
|
@@ -912,6 +958,9 @@ async def analyze_game(request: AnalyzeRequest):
|
|
| 912 |
except Exception as e:
|
| 913 |
print(f"Analysis Error: {e}")
|
| 914 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
| 915 |
|
| 916 |
|
| 917 |
if __name__ == "__main__":
|
|
|
|
| 122 |
_ENGINE_LOCK = asyncio.Lock()
|
| 123 |
_ENGINE_IO_LOCK = asyncio.Lock()
|
| 124 |
|
| 125 |
+
# Global state for monitoring
|
| 126 |
+
_PENDING_ENGINE_REQUESTS = 0
|
| 127 |
+
_MAX_PENDING_ENGINE_REQUESTS = 10 # Drop requests if queue is too long to prevent OOM
|
| 128 |
+
_MAX_PENDING_GAME_REVIEWS = 2 # Max concurrent full game reviews (heavy RAM)
|
| 129 |
+
_PENDING_GAME_REVIEWS = 0
|
| 130 |
+
_LOCK_TIMEOUT_SEC = 20.0 # Max wait time in queue before giving up
|
| 131 |
+
|
| 132 |
|
| 133 |
def _engine_hash_mb() -> int:
|
| 134 |
try:
|
|
|
|
| 463 |
# βββ Bot Move (/move) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 464 |
@app.post("/move", response_model=MoveResponse)
|
| 465 |
async def get_move(request: MoveRequest):
|
| 466 |
+
global _PENDING_ENGINE_REQUESTS
|
| 467 |
+
if _PENDING_ENGINE_REQUESTS > _MAX_PENDING_ENGINE_REQUESTS:
|
| 468 |
+
force_memory_release()
|
| 469 |
+
raise HTTPException(status_code=429, detail="Server busy β too many analysis requests.")
|
| 470 |
+
|
| 471 |
+
_PENDING_ENGINE_REQUESTS += 1
|
| 472 |
try:
|
| 473 |
engine = await get_deepcastle_engine()
|
| 474 |
board = chess.Board(request.fen)
|
| 475 |
limit = chess.engine.Limit(time=request.time, depth=request.depth)
|
| 476 |
tsec = _search_timeout_sec(request.time, request.depth)
|
| 477 |
|
| 478 |
+
try:
|
| 479 |
+
async with asyncio.timeout(_LOCK_TIMEOUT_SEC):
|
| 480 |
+
async with _ENGINE_IO_LOCK:
|
| 481 |
+
result = await _engine_call(
|
| 482 |
+
engine,
|
| 483 |
+
engine.play(board, limit, info=chess.engine.INFO_ALL),
|
| 484 |
+
tsec,
|
| 485 |
+
)
|
| 486 |
+
info = dict(result.info)
|
| 487 |
+
if not info:
|
| 488 |
+
info = await _engine_call(
|
| 489 |
+
engine,
|
| 490 |
+
engine.analyse(board, limit, info=chess.engine.INFO_ALL),
|
| 491 |
+
tsec,
|
| 492 |
+
)
|
| 493 |
+
except asyncio.TimeoutError:
|
| 494 |
+
raise HTTPException(status_code=503, detail="Server overloaded β lock wait timeout.")
|
| 495 |
|
| 496 |
score_cp, mate_in = get_normalized_score(info)
|
| 497 |
depth, nodes, nps = normalize_search_stats(info)
|
|
|
|
| 517 |
|
| 518 |
del result
|
| 519 |
del info
|
| 520 |
+
|
| 521 |
+
# Match Shield: Force cleanup and hash clear after every move to prevent "Idle Growth"
|
| 522 |
+
async with _ENGINE_IO_LOCK:
|
| 523 |
+
await _clear_engine_hash(engine)
|
| 524 |
+
force_memory_release()
|
| 525 |
|
| 526 |
return MoveResponse(
|
| 527 |
bestmove=best_move,
|
|
|
|
| 538 |
except Exception as e:
|
| 539 |
print(f"Error: {e}")
|
| 540 |
raise HTTPException(status_code=500, detail=str(e))
|
| 541 |
+
finally:
|
| 542 |
+
_PENDING_ENGINE_REQUESTS -= 1
|
| 543 |
|
| 544 |
|
| 545 |
# βββ Hint Move (/analysis-move) βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 546 |
@app.post("/analysis-move", response_model=MoveResponse)
|
| 547 |
async def get_analysis_move(request: MoveRequest):
|
| 548 |
+
global _PENDING_ENGINE_REQUESTS
|
| 549 |
+
if _PENDING_ENGINE_REQUESTS > _MAX_PENDING_ENGINE_REQUESTS:
|
| 550 |
+
force_memory_release()
|
| 551 |
+
raise HTTPException(status_code=429, detail="Server busy β too many analysis requests.")
|
| 552 |
+
|
| 553 |
+
_PENDING_ENGINE_REQUESTS += 1
|
| 554 |
try:
|
| 555 |
engine = await get_stockfish_engine()
|
| 556 |
board = chess.Board(request.fen)
|
| 557 |
limit = chess.engine.Limit(time=request.time, depth=request.depth)
|
| 558 |
tsec = _search_timeout_sec(request.time, request.depth)
|
| 559 |
|
| 560 |
+
try:
|
| 561 |
+
async with asyncio.timeout(_LOCK_TIMEOUT_SEC):
|
| 562 |
+
async with _ENGINE_IO_LOCK:
|
| 563 |
+
result = await _engine_call(
|
| 564 |
+
engine,
|
| 565 |
+
engine.play(board, limit, info=chess.engine.INFO_ALL),
|
| 566 |
+
tsec,
|
| 567 |
+
)
|
| 568 |
+
info = dict(result.info)
|
| 569 |
+
if not info:
|
| 570 |
+
info = await _engine_call(
|
| 571 |
+
engine,
|
| 572 |
+
engine.analyse(board, limit, info=chess.engine.INFO_ALL),
|
| 573 |
+
tsec,
|
| 574 |
+
)
|
| 575 |
+
except asyncio.TimeoutError:
|
| 576 |
+
raise HTTPException(status_code=503, detail="Server overloaded β lock wait timeout.")
|
| 577 |
|
| 578 |
score_cp, mate_in = get_normalized_score(info)
|
| 579 |
depth, nodes, nps = normalize_search_stats(info)
|
|
|
|
| 620 |
except Exception as e:
|
| 621 |
print(f"Analysis move error: {e}")
|
| 622 |
raise HTTPException(status_code=500, detail=str(e))
|
| 623 |
+
finally:
|
| 624 |
+
_PENDING_ENGINE_REQUESTS -= 1
|
| 625 |
|
| 626 |
|
| 627 |
# βββ Openings DB βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 777 |
# βββ Game Analysis (/analyze-game) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 778 |
@app.post("/analyze-game", response_model=AnalyzeResponse)
|
| 779 |
async def analyze_game(request: AnalyzeRequest):
|
| 780 |
+
global _PENDING_ENGINE_REQUESTS, _PENDING_GAME_REVIEWS
|
| 781 |
+
|
| 782 |
+
if _PENDING_GAME_REVIEWS >= _MAX_PENDING_GAME_REVIEWS:
|
| 783 |
+
raise HTTPException(status_code=429, detail="Analysis queue full. Only 2 games can be reviewed at once.")
|
| 784 |
+
|
| 785 |
+
if _PENDING_ENGINE_REQUESTS >= _MAX_PENDING_ENGINE_REQUESTS:
|
| 786 |
+
raise HTTPException(status_code=429, detail="Server busy β too many moving parts.")
|
| 787 |
+
|
| 788 |
+
_PENDING_ENGINE_REQUESTS += 1
|
| 789 |
+
_PENDING_GAME_REVIEWS += 1
|
| 790 |
try:
|
| 791 |
engine = await get_stockfish_engine()
|
| 792 |
board = chess.Board(request.start_fen) if request.start_fen else chess.Board()
|
|
|
|
| 958 |
except Exception as e:
|
| 959 |
print(f"Analysis Error: {e}")
|
| 960 |
raise HTTPException(status_code=500, detail=str(e))
|
| 961 |
+
finally:
|
| 962 |
+
_PENDING_ENGINE_REQUESTS -= 1
|
| 963 |
+
_PENDING_GAME_REVIEWS -= 1
|
| 964 |
|
| 965 |
|
| 966 |
if __name__ == "__main__":
|