suvadityamuk HF Staff commited on
Commit
de594a8
·
verified ·
1 Parent(s): c9dc459

Upload blender_session.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
- size = os.path.getsize(self.checkpoint_path)
244
- except OSError:
245
- size = 0
246
- if (
247
- size > _CHECKPOINT_LARGE_BYTES
248
- and time.time() - self._last_checkpoint < _CHECKPOINT_MIN_INTERVAL
249
- ):
250
- return
251
- try:
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
- limit, used = container_memory()
452
- if limit and (limit - used) < limit * _MIN_FREE_MEM_FRACTION:
453
- raise RuntimeError(
454
- "The server is low on memory right now (existing scenes are very "
455
- "heavy) - please try again in a few minutes."
456
- )
457
- session = BlenderSession(session_id)
 
 
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