mirror of
https://github.com/blw1138/Zordon.git
synced 2026-06-09 13:39:24 -05:00
refactor: wire all services through ApplicationContext
- Created src/application_context.py as DI container with TYPE_CHECKING imports - server.py now instantiates all services in dependency order via ApplicationContext - Fixed infinite recursion bug: 48 instance methods renamed with underscore prefix to avoid shadowing by same-named @classmethod forwarders - ZeroconfServer: instantiate Zeroconf() in __init__, add _sync_class() to configure forwarder, direct _configure/_start calls during wiring - Config, EngineManager, PreviewManager: all forwarders and _sync_class() intact - RenderQueue: load_state and subscribe moved to __init__, threading.Lock retained - DistributedJobManager: subscribe_to_listener moved to __init__
This commit is contained in:
+194
-107
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
import threading
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pubsub import pub
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||
|
||||
from src.engines.core.base_worker import Base, BaseRenderWorker
|
||||
@@ -24,184 +26,269 @@ class JobNotFoundError(Exception):
|
||||
|
||||
|
||||
class RenderQueue:
|
||||
engine: Optional[create_engine] = None
|
||||
session: Optional[sessionmaker] = None
|
||||
job_queue: List[BaseRenderWorker] = []
|
||||
maximum_renderer_instances: Dict[str, int] = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
|
||||
last_saved_counts: Dict[str, int] = {}
|
||||
is_running: bool = False
|
||||
_default_instance: Optional['RenderQueue'] = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.engine: Optional[create_engine] = None
|
||||
self.session: Optional[Session] = None
|
||||
self.job_queue: List[BaseRenderWorker] = []
|
||||
self.maximum_renderer_instances: Dict[str, int] = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
|
||||
self.last_saved_counts: Dict[str, int] = {}
|
||||
self.is_running: bool = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# --------------------------------------------
|
||||
# Render Queue Evaluation:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
"""Start evaluating the render queue"""
|
||||
def _start(self) -> None:
|
||||
logger.debug("Starting render queue updates")
|
||||
cls.is_running = True
|
||||
cls.evaluate_queue()
|
||||
self.is_running = True
|
||||
self.evaluate_queue()
|
||||
|
||||
@classmethod
|
||||
def evaluate_queue(cls):
|
||||
def _evaluate_queue(self) -> None:
|
||||
try:
|
||||
not_started = cls.jobs_with_status(RenderStatus.NOT_STARTED, priority_sorted=True)
|
||||
not_started = self.jobs_with_status(RenderStatus.NOT_STARTED, priority_sorted=True)
|
||||
for job in not_started:
|
||||
if cls.is_available_for_job(job.engine_name, job.priority):
|
||||
cls.start_job(job)
|
||||
if self.is_available_for_job(job.engine_name, job.priority):
|
||||
self.start_job(job)
|
||||
|
||||
scheduled = cls.jobs_with_status(RenderStatus.SCHEDULED, priority_sorted=True)
|
||||
scheduled = self.jobs_with_status(RenderStatus.SCHEDULED, priority_sorted=True)
|
||||
for job in scheduled:
|
||||
if job.scheduled_start <= datetime.now():
|
||||
logger.debug(f"Starting scheduled job: {job}")
|
||||
cls.start_job(job)
|
||||
self.start_job(job)
|
||||
|
||||
if cls.last_saved_counts != cls.job_counts():
|
||||
cls.save_state()
|
||||
if self.last_saved_counts != self.job_counts():
|
||||
self.save_state()
|
||||
except DetachedInstanceError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def __local_job_status_changed(cls, job_id, old_status, new_status):
|
||||
render_job = RenderQueue.job_with_id(job_id, none_ok=True)
|
||||
if render_job and cls.is_running: # ignore changes from render jobs not in the queue yet
|
||||
def _local_job_status_changed(self, job_id: str, old_status: str, new_status: str) -> None:
|
||||
render_job = self.job_with_id(job_id, none_ok=True)
|
||||
if render_job and self.is_running:
|
||||
logger.debug(f"RenderQueue detected job {job_id} has changed from {old_status} -> {new_status}")
|
||||
RenderQueue.evaluate_queue()
|
||||
self.evaluate_queue()
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
def _stop(self) -> None:
|
||||
logger.debug("Stopping render queue updates")
|
||||
cls.is_running = False
|
||||
self.is_running = False
|
||||
|
||||
# --------------------------------------------
|
||||
# Fetch Jobs:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def all_jobs(cls):
|
||||
return cls.job_queue
|
||||
def _all_jobs(self) -> List[BaseRenderWorker]:
|
||||
return self.job_queue
|
||||
|
||||
@classmethod
|
||||
def running_jobs(cls):
|
||||
return cls.jobs_with_status(RenderStatus.RUNNING)
|
||||
def _running_jobs(self) -> List[BaseRenderWorker]:
|
||||
return self.jobs_with_status(RenderStatus.RUNNING)
|
||||
|
||||
@classmethod
|
||||
def pending_jobs(cls):
|
||||
pending_jobs = cls.jobs_with_status(RenderStatus.NOT_STARTED)
|
||||
pending_jobs.extend(cls.jobs_with_status(RenderStatus.SCHEDULED))
|
||||
return pending_jobs
|
||||
def _pending_jobs(self) -> List[BaseRenderWorker]:
|
||||
pending = self.jobs_with_status(RenderStatus.NOT_STARTED)
|
||||
pending.extend(self.jobs_with_status(RenderStatus.SCHEDULED))
|
||||
return pending
|
||||
|
||||
@classmethod
|
||||
def jobs_with_status(cls, status, priority_sorted=False):
|
||||
found_jobs = [x for x in cls.all_jobs() if x.status == status]
|
||||
def _jobs_with_status(self, status: RenderStatus, priority_sorted: bool = False) -> List[BaseRenderWorker]:
|
||||
found_jobs = [x for x in self.all_jobs() if x.status == status]
|
||||
if priority_sorted:
|
||||
found_jobs = sorted(found_jobs, key=lambda a: a.priority, reverse=False)
|
||||
return found_jobs
|
||||
|
||||
@classmethod
|
||||
def job_with_id(cls, job_id, none_ok=False):
|
||||
found_job = next((x for x in cls.all_jobs() if x.id == job_id), None)
|
||||
def _job_with_id(self, job_id: str, none_ok: bool = False) -> Optional[BaseRenderWorker]:
|
||||
found_job = next((x for x in self.all_jobs() if x.id == job_id), None)
|
||||
if not found_job and not none_ok:
|
||||
raise JobNotFoundError(job_id)
|
||||
return found_job
|
||||
|
||||
@classmethod
|
||||
def job_counts(cls):
|
||||
job_counts = {}
|
||||
for job_status in RenderStatus:
|
||||
job_counts[job_status.value] = len(cls.jobs_with_status(job_status))
|
||||
return job_counts
|
||||
def _job_counts(self) -> Dict[str, int]:
|
||||
counts = Counter(x.status for x in self.all_jobs())
|
||||
return {s.value: counts.get(s, 0) for s in RenderStatus}
|
||||
|
||||
# --------------------------------------------
|
||||
# Startup / Shutdown:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def load_state(cls, database_directory: Path):
|
||||
if not cls.engine:
|
||||
cls.engine = create_engine(f"sqlite:///{database_directory / 'database.db'}")
|
||||
Base.metadata.create_all(cls.engine)
|
||||
cls.session = sessionmaker(bind=cls.engine)()
|
||||
def _load_state(self, database_directory: Path) -> None:
|
||||
self.engine = create_engine(f"sqlite:///{database_directory / 'database.db'}")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session = sessionmaker(bind=self.engine)()
|
||||
from src.engines.core.base_worker import BaseRenderWorker
|
||||
cls.job_queue = cls.session.query(BaseRenderWorker).all()
|
||||
pub.subscribe(cls.__local_job_status_changed, 'status_change')
|
||||
self.job_queue = self.session.query(BaseRenderWorker).all()
|
||||
pub.subscribe(self._local_job_status_changed, 'status_change')
|
||||
|
||||
@classmethod
|
||||
def save_state(cls):
|
||||
cls.session.commit()
|
||||
def _save_state(self) -> None:
|
||||
if self.session:
|
||||
self.session.commit()
|
||||
|
||||
@classmethod
|
||||
def prepare_for_shutdown(cls):
|
||||
def _prepare_for_shutdown(self) -> None:
|
||||
logger.debug("Closing session")
|
||||
cls.stop()
|
||||
running_jobs = cls.jobs_with_status(RenderStatus.RUNNING) # cancel all running jobs
|
||||
[cls.cancel_job(job) for job in running_jobs]
|
||||
cls.save_state()
|
||||
cls.session.close()
|
||||
self.stop()
|
||||
running_jobs = self.jobs_with_status(RenderStatus.RUNNING)
|
||||
for job in running_jobs:
|
||||
self.cancel_job(job)
|
||||
self.save_state()
|
||||
if self.session:
|
||||
self.session.close()
|
||||
|
||||
# --------------------------------------------
|
||||
# Renderer Availability:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def renderer_instances(cls):
|
||||
from collections import Counter
|
||||
all_instances = [x.engine_name for x in cls.running_jobs()]
|
||||
def renderer_instances(self) -> Counter:
|
||||
all_instances = [x.engine_name for x in self.running_jobs()]
|
||||
return Counter(all_instances)
|
||||
|
||||
@classmethod
|
||||
def is_available_for_job(cls, renderer, priority=2):
|
||||
|
||||
instances = cls.renderer_instances()
|
||||
higher_priority_jobs = [x for x in cls.running_jobs() if x.priority < priority]
|
||||
max_allowed_instances = cls.maximum_renderer_instances.get(renderer, 1)
|
||||
maxed_out_instances = renderer in instances.keys() and instances[renderer] >= max_allowed_instances
|
||||
def _is_available_for_job(self, renderer: str, priority: int = 2) -> bool:
|
||||
instances = self.renderer_instances()
|
||||
higher_priority_jobs = [x for x in self.running_jobs() if x.priority < priority]
|
||||
max_allowed_instances = self.maximum_renderer_instances.get(renderer, 1)
|
||||
maxed_out_instances = renderer in instances and instances[renderer] >= max_allowed_instances
|
||||
return not maxed_out_instances and not higher_priority_jobs
|
||||
|
||||
# --------------------------------------------
|
||||
# Job Lifecycle Management:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def add_to_render_queue(cls, render_job, force_start=False):
|
||||
def _add_to_render_queue(self, render_job: BaseRenderWorker, force_start: bool = False) -> None:
|
||||
logger.info(f"Adding job to render queue: {render_job}")
|
||||
cls.job_queue.append(render_job)
|
||||
if cls.is_running and force_start and render_job.status in (RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED):
|
||||
cls.start_job(render_job)
|
||||
cls.session.add(render_job)
|
||||
cls.save_state()
|
||||
if cls.is_running:
|
||||
cls.evaluate_queue()
|
||||
with self._lock:
|
||||
self.job_queue.append(render_job)
|
||||
if force_start and render_job.status in (RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED):
|
||||
self.start_job(render_job)
|
||||
self.session.add(render_job)
|
||||
self.save_state()
|
||||
if self.is_running:
|
||||
self.evaluate_queue()
|
||||
|
||||
@classmethod
|
||||
def start_job(cls, job):
|
||||
def _start_job(self, job: BaseRenderWorker) -> None:
|
||||
logger.info(f'Starting job: {job}')
|
||||
job.start()
|
||||
cls.save_state()
|
||||
self.save_state()
|
||||
|
||||
@classmethod
|
||||
def cancel_job(cls, job):
|
||||
def _cancel_job(self, job: BaseRenderWorker) -> bool:
|
||||
logger.info(f'Cancelling job: {job}')
|
||||
job.stop()
|
||||
return job.status == RenderStatus.CANCELLED
|
||||
|
||||
@classmethod
|
||||
def delete_job(cls, job):
|
||||
def _delete_job(self, job: BaseRenderWorker) -> bool:
|
||||
logger.info(f"Deleting job: {job}")
|
||||
job.stop()
|
||||
cls.job_queue.remove(job)
|
||||
cls.session.delete(job)
|
||||
cls.save_state()
|
||||
with self._lock:
|
||||
job.stop()
|
||||
self.job_queue.remove(job)
|
||||
self.session.delete(job)
|
||||
self.save_state()
|
||||
return True
|
||||
|
||||
# --------------------------------------------
|
||||
# Miscellaneous:
|
||||
# --------------------------------------------
|
||||
|
||||
def _clear_history(self) -> None:
|
||||
for job in list(self.all_jobs()):
|
||||
if job.status in (RenderStatus.CANCELLED, RenderStatus.COMPLETED, RenderStatus.ERROR):
|
||||
self.delete_job(job)
|
||||
self.save_state()
|
||||
|
||||
# --- Forwarders for backward compatibility ---
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._start()
|
||||
|
||||
@classmethod
|
||||
def evaluate_queue(cls):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._evaluate_queue()
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._stop()
|
||||
|
||||
@classmethod
|
||||
def all_jobs(cls):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance.job_queue
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def running_jobs(cls):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._running_jobs()
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def pending_jobs(cls):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._pending_jobs()
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def jobs_with_status(cls, status, priority_sorted=False):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._jobs_with_status(status, priority_sorted)
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def job_with_id(cls, job_id, none_ok=False):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._job_with_id(job_id, none_ok)
|
||||
if not none_ok:
|
||||
raise JobNotFoundError(job_id)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def job_counts(cls):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._job_counts()
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def load_state(cls, database_directory):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._load_state(database_directory)
|
||||
|
||||
@classmethod
|
||||
def save_state(cls):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._save_state()
|
||||
|
||||
@classmethod
|
||||
def prepare_for_shutdown(cls):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._prepare_for_shutdown()
|
||||
|
||||
@classmethod
|
||||
def is_available_for_job(cls, renderer, priority=2):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._is_available_for_job(renderer, priority)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def add_to_render_queue(cls, render_job, force_start=False):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._add_to_render_queue(render_job, force_start)
|
||||
|
||||
@classmethod
|
||||
def start_job(cls, job):
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._start_job(job)
|
||||
|
||||
@classmethod
|
||||
def cancel_job(cls, job):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._cancel_job(job)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def delete_job(cls, job):
|
||||
if cls._default_instance is not None:
|
||||
return cls._default_instance._delete_job(job)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def clear_history(cls):
|
||||
to_remove = [x for x in cls.all_jobs() if x.status in [RenderStatus.CANCELLED,
|
||||
RenderStatus.COMPLETED, RenderStatus.ERROR]]
|
||||
for job_to_remove in to_remove:
|
||||
cls.delete_job(job_to_remove)
|
||||
cls.save_state()
|
||||
if cls._default_instance is not None:
|
||||
cls._default_instance._clear_history()
|
||||
|
||||
Reference in New Issue
Block a user