Spaces:
Running on Zero
Running on Zero
Upload blender_session.py with huggingface_hub
Browse files- blender_session.py +69 -16
blender_session.py
CHANGED
|
@@ -215,6 +215,7 @@ class BlenderSession:
|
|
| 215 |
self.last_validation: dict | None = None
|
| 216 |
self.checkpoint_path = os.path.join(self.workdir, "checkpoint.blend")
|
| 217 |
self._last_checkpoint = 0.0
|
|
|
|
| 218 |
self._log_path = os.path.join(self.workdir, "blender.log")
|
| 219 |
self._spawn()
|
| 220 |
|
|
@@ -239,16 +240,18 @@ class BlenderSession:
|
|
| 239 |
checkpoint must never fail the tool call that triggered it. Large
|
| 240 |
checkpoints are time-throttled to keep heavy scenes responsive.
|
| 241 |
"""
|
|
|
|
|
|
|
| 242 |
try:
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
self.send_code(
|
| 253 |
"import bpy\n"
|
| 254 |
"bpy.ops.wm.save_as_mainfile("
|
|
@@ -260,6 +263,8 @@ class BlenderSession:
|
|
| 260 |
self._last_checkpoint = time.time()
|
| 261 |
except Exception: # noqa: BLE001
|
| 262 |
log.warning("Checkpoint save failed for session %s", self.session_id, exc_info=True)
|
|
|
|
|
|
|
| 263 |
|
| 264 |
def restart(self) -> bool:
|
| 265 |
"""
|
|
@@ -424,6 +429,26 @@ _sessions: dict[str, BlenderSession] = {}
|
|
| 424 |
_registry_lock = threading.Lock()
|
| 425 |
_reaper_started = False
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
def get_session(session_id: str) -> BlenderSession:
|
| 429 |
"""Return the live session for this id, creating one if needed."""
|
|
@@ -448,13 +473,15 @@ def get_session(session_id: str) -> BlenderSession:
|
|
| 448 |
f"All {MAX_SESSIONS} Blender slots are busy right now - please try again "
|
| 449 |
"in a few minutes."
|
| 450 |
)
|
| 451 |
-
|
| 452 |
-
if
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
| 458 |
_sessions[session_id] = session
|
| 459 |
return session
|
| 460 |
|
|
@@ -490,8 +517,34 @@ def _reaper_loop() -> None:
|
|
| 490 |
session.close()
|
| 491 |
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
def start_reaper() -> None:
|
| 494 |
global _reaper_started
|
| 495 |
if not _reaper_started:
|
| 496 |
threading.Thread(target=_reaper_loop, daemon=True, name="blender-reaper").start()
|
|
|
|
| 497 |
_reaper_started = True
|
|
|
|
| 215 |
self.last_validation: dict | None = None
|
| 216 |
self.checkpoint_path = os.path.join(self.workdir, "checkpoint.blend")
|
| 217 |
self._last_checkpoint = 0.0
|
| 218 |
+
self._checkpoint_busy = threading.Lock()
|
| 219 |
self._log_path = os.path.join(self.workdir, "blender.log")
|
| 220 |
self._spawn()
|
| 221 |
|
|
|
|
| 240 |
checkpoint must never fail the tool call that triggered it. Large
|
| 241 |
checkpoints are time-throttled to keep heavy scenes responsive.
|
| 242 |
"""
|
| 243 |
+
if not self._checkpoint_busy.acquire(blocking=False):
|
| 244 |
+
return # one already in flight; the newer state gets the next save
|
| 245 |
try:
|
| 246 |
+
try:
|
| 247 |
+
size = os.path.getsize(self.checkpoint_path)
|
| 248 |
+
except OSError:
|
| 249 |
+
size = 0
|
| 250 |
+
if (
|
| 251 |
+
size > _CHECKPOINT_LARGE_BYTES
|
| 252 |
+
and time.time() - self._last_checkpoint < _CHECKPOINT_MIN_INTERVAL
|
| 253 |
+
):
|
| 254 |
+
return
|
| 255 |
self.send_code(
|
| 256 |
"import bpy\n"
|
| 257 |
"bpy.ops.wm.save_as_mainfile("
|
|
|
|
| 263 |
self._last_checkpoint = time.time()
|
| 264 |
except Exception: # noqa: BLE001
|
| 265 |
log.warning("Checkpoint save failed for session %s", self.session_id, exc_info=True)
|
| 266 |
+
finally:
|
| 267 |
+
self._checkpoint_busy.release()
|
| 268 |
|
| 269 |
def restart(self) -> bool:
|
| 270 |
"""
|
|
|
|
| 429 |
_registry_lock = threading.Lock()
|
| 430 |
_reaper_started = False
|
| 431 |
|
| 432 |
+
# One spare session is kept warm in the background so a visitor's first message
|
| 433 |
+
# doesn't pay Blender's ~10s cold start.
|
| 434 |
+
_prewarmed: BlenderSession | None = None
|
| 435 |
+
_prewarm_lock = threading.Lock()
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def _claim_prewarmed(session_id: str) -> BlenderSession | None:
|
| 439 |
+
global _prewarmed
|
| 440 |
+
with _prewarm_lock:
|
| 441 |
+
session = _prewarmed
|
| 442 |
+
_prewarmed = None
|
| 443 |
+
if session is None:
|
| 444 |
+
return None
|
| 445 |
+
if not session.alive:
|
| 446 |
+
session.close()
|
| 447 |
+
return None
|
| 448 |
+
session.session_id = session_id
|
| 449 |
+
session.touch()
|
| 450 |
+
return session
|
| 451 |
+
|
| 452 |
|
| 453 |
def get_session(session_id: str) -> BlenderSession:
|
| 454 |
"""Return the live session for this id, creating one if needed."""
|
|
|
|
| 473 |
f"All {MAX_SESSIONS} Blender slots are busy right now - please try again "
|
| 474 |
"in a few minutes."
|
| 475 |
)
|
| 476 |
+
session = _claim_prewarmed(session_id)
|
| 477 |
+
if session is None:
|
| 478 |
+
limit, used = container_memory()
|
| 479 |
+
if limit and (limit - used) < limit * _MIN_FREE_MEM_FRACTION:
|
| 480 |
+
raise RuntimeError(
|
| 481 |
+
"The server is low on memory right now (existing scenes are very "
|
| 482 |
+
"heavy) - please try again in a few minutes."
|
| 483 |
+
)
|
| 484 |
+
session = BlenderSession(session_id)
|
| 485 |
_sessions[session_id] = session
|
| 486 |
return session
|
| 487 |
|
|
|
|
| 517 |
session.close()
|
| 518 |
|
| 519 |
|
| 520 |
+
def _prewarm_loop() -> None:
|
| 521 |
+
global _prewarmed
|
| 522 |
+
while True:
|
| 523 |
+
try:
|
| 524 |
+
with _prewarm_lock:
|
| 525 |
+
if _prewarmed is not None and not _prewarmed.alive:
|
| 526 |
+
_prewarmed.close()
|
| 527 |
+
_prewarmed = None
|
| 528 |
+
have_spare = _prewarmed is not None
|
| 529 |
+
with _registry_lock:
|
| 530 |
+
live = sum(1 for s in _sessions.values() if s.alive)
|
| 531 |
+
limit, used = container_memory()
|
| 532 |
+
memory_ok = not limit or (limit - used) >= limit * _MIN_FREE_MEM_FRACTION
|
| 533 |
+
# Leave one slot of headroom so the spare never blocks a real visitor.
|
| 534 |
+
if not have_spare and live < MAX_SESSIONS - 1 and memory_ok:
|
| 535 |
+
spare = BlenderSession("prewarm")
|
| 536 |
+
with _prewarm_lock:
|
| 537 |
+
_prewarmed = spare
|
| 538 |
+
log.info("Prewarmed spare Blender session (port %d)", spare.port)
|
| 539 |
+
except Exception: # noqa: BLE001
|
| 540 |
+
log.exception("Prewarm attempt failed")
|
| 541 |
+
time.sleep(30)
|
| 542 |
+
time.sleep(5)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
def start_reaper() -> None:
|
| 546 |
global _reaper_started
|
| 547 |
if not _reaper_started:
|
| 548 |
threading.Thread(target=_reaper_loop, daemon=True, name="blender-reaper").start()
|
| 549 |
+
threading.Thread(target=_prewarm_loop, daemon=True, name="blender-prewarm").start()
|
| 550 |
_reaper_started = True
|